Chapter 30: Suspended Execution Model
生成器把一次函数调用拆成多次恢复执行。读完本章后,读者应能追踪一个包含 yield 的函数从调用、首次恢复、挂起、再次恢复到关闭的完整路径,并能判断局部变量、异常、返回值和委托调用分别保存在哪里、何时传播、何时释放。
本章的贯穿材料是一段累加器生成器。它的代码现象很小:函数调用先返回一个 generator object,next() 或 send() 才推进函数体;yield 返回一个值给调用方,同时把当前 frame 留在可恢复位置;后续 send() 会把值送回那个 yield 表达式。这个材料足以覆盖 suspended frame、四类交互、yield from 委托、惰性执行和生成器与协程的边界。
Python 语言层面把这组行为定义在 yield expressions 和 generator-iterator methods 中;PEP 342 扩展了 send()、throw()、close(),PEP 380 定义了 yield from 的委托语义。CPython 实现会把这些语义落到 generator object、frame 状态、指令位置和异常路径上。不同 CPython 版本的内部 frame 组织会变化,本文只依赖 Python 语义和可观察的 generator 状态。
贯穿示例如下。它证明的核心点是:total 是 generator frame 中的局部变量,调用方每次恢复生成器时都回到上一次 yield 停住的位置。
def accumulator():
total = 0
try:
while True:
incoming = yield total
if incoming is None:
incoming = 1
total += incoming
finally:
print("closing", total)
g = accumulator()
first = next(g) # 0
second = g.send(5) # 5
third = g.send(None) # 6
g.close() # prints: closing 6
这段代码中,accumulator() 这次调用先创建 generator object。函数体中的 total = 0 在 next(g) 前尚未执行。next(g) 进入 frame,运行到 yield total,把 0 返回给调用方,并把 frame 挂起在这个表达式位置。g.send(5) 让上一次的 yield total 表达式求值为 5,于是 incoming 得到 5,total 更新为 5,循环再次执行到 yield total 并挂起。
下面的状态图只表达生成器主状态迁移,不覆盖 async generator 和 event loop 调度。图中每条边都对应调用方对 generator object 的一次协议调用,或者 generator frame 内部遇到 yield、return、异常和关闭路径。
Created 表示 generator object 已经存在,函数体还没有开始执行。Running 表示当前 frame 正在解释器中推进,外部同一个 generator 入口暂时受 running flag 保护。Suspended 表示 frame 保存了 instruction pointer、局部变量和表达式中间状态,等待下一次恢复。Closed 表示 frame 已经完成返回、被关闭,或者被未捕获异常终止。
30.1 Generator object and suspended frame
Generator object 是一次可暂停函数调用的运行时载体。一个函数体内出现 yield 或 yield from 后,调用这个函数产生 generator object;这个对象实现 iterator 协议,同时保存恢复执行所需的状态。这里的“保存”包括两个层次:语言层面保存下一次恢复的位置和局部变量语义;CPython 层面通过 generator 相关对象保存 frame 状态、运行标记和关闭状态。
在贯穿示例中,accumulator() 这次调用没有立即执行函数体。调用结果 g 是 generator object,它可以被 next(g)、g.send(value)、g.throw(...) 和 g.close() 驱动。这个对象有自己的生命周期;只要 g 仍被引用,挂起中的 frame 和 frame 内局部对象就有继续存活的依据。
Suspended frame 是理解生成器的关键对象。普通函数调用进入 frame 后会连续运行到 return 或异常路径,调用方只看到最终结果或异常。生成器 frame 在 yield 处暂停,调用栈返回到调用方,但 frame 内的局部变量、当前指令位置和必要的求值状态仍保留在 generator object 关联的执行状态中。下一次恢复时,解释器从这个保存点继续推进。
可以用 Python 层的可观察状态辅助验证这件事。inspect.getgeneratorstate() 会把生成器分为 GEN_CREATED、GEN_RUNNING、GEN_SUSPENDED 和 GEN_CLOSED,这些状态名称来自标准库 inspect 文档。它们不是 CPython 内部结构的完整展开,却足以表达用户代码能观察到的生命周期边界。
import inspect
g = accumulator()
created = inspect.getgeneratorstate(g) # GEN_CREATED
next(g)
suspended = inspect.getgeneratorstate(g) # GEN_SUSPENDED
g.close()
closed = inspect.getgeneratorstate(g) # GEN_CLOSED
这段观察代码说明三个判断。第一,创建 generator object 只建立可恢复调用对象,函数体尚未推进。第二,执行到 yield 后,调用权回到外部,generator 的状态变为 suspended。第三,关闭后继续保存 g 这个名字也只能保存一个已关闭的 generator object,不能让 frame 回到可恢复执行路径。
从 CPython 阅读角度看,应把 generator object 和 frame 分开判断。generator object 是外部协议入口,负责接收 next()、send()、throw() 和 close()。frame 是代码执行状态,负责保存 code object、locals、value stack、instruction pointer 和异常相关状态。CPython 3.11 之后内部 frame 表示经历过调整,Python 层仍可通过 generator object 的状态、gi_frame 可见性和 inspect 观察主要生命周期。
生成器还需要 re-entry 边界。一个 generator 正在 Running 状态时,同一个 generator 再次被恢复会触发运行时错误。这个规则保护的是同一个 suspended frame 的一致性:frame 只有一份局部变量和一条 instruction pointer,解释器一次只能沿一条路径推进它。
30.2 yield, send, throw, and close
yield 是生成器暂停点,也是调用方和生成器交换数据的表达式。它有两个方向:向外返回一个值,向内接收下一次恢复时送入的值。贯穿示例中的 incoming = yield total 把这两个方向放在同一行:total 向外返回,下一次 send() 的参数会成为这整个 yield total 表达式的求值结果。
next(g) 与 g.send(None) 都可以启动一个刚创建的生成器。刚创建时 frame 尚未执行到第一个 yield,此时没有等待接收值的 yield 表达式。由此得到一条稳定规则:首次恢复使用 next(g) 或 send(None);带非空值的 send(value) 应在生成器已经停在某个 yield 表达式之后使用。
g = accumulator()
next(g) # 运行到 yield total,返回 0
g.send(5) # 上一次 yield 表达式得到 5,total 更新到 5,再 yield 5
g.send(None) # 上一次 yield 表达式得到 None,代码改成 1,total 更新到 6,再 yield 6
这段序列说明 send() 的返回值和参数属于两个不同方向。g.send(5) 的参数 5 进入生成器内部,成为 incoming。g.send(5) 的调用返回值来自生成器下一次执行到的 yield total。如果中间代码运行到 return,调用方会收到 StopIteration,返回值附在异常对象上。
throw() 把异常注入到当前挂起点。它的效果可以理解为:上一次停住的 yield 表达式在恢复时直接抛出调用方指定的异常。生成器内部可以捕获这个异常、执行清理、继续 yield,或者让异常向调用方传播。
def guarded():
try:
yield "ready"
except ValueError as exc:
yield f"handled: {exc}"
yield "done"
it = guarded()
next(it) # "ready"
it.throw(ValueError("bad")) # "handled: bad"
next(it) # "done"
这个例子中,throw() 没有绕开 generator frame。异常从 suspended point 进入 frame,except ValueError 在生成器内部处理它,然后生成器继续运行到下一个 yield。如果内部没有匹配的 handler,异常会穿过 generator object 传播给调用方,生成器通常进入 closed 状态。
close() 请求生成器结束。语言语义上,它会在挂起位置引发 GeneratorExit,让 finally 和上下文清理路径有机会执行。贯穿示例里 g.close() 会触发 finally,打印当前 total。清理代码应完成资源释放并结束;在关闭期间继续产出普通值会被解释器视为错误路径。
g = accumulator()
next(g)
g.send(5)
g.close() # finally 路径运行,输出 closing 5
四类交互可以按同一组维度判断:恢复入口、进入 frame 的值或异常、frame 内部推进结果、调用方看到的输出。yield 产生向外的普通值;send() 产生向内的普通值并等待下一次向外产出;throw() 产生向内的异常;close() 产生向内的关闭请求并触发清理。掌握这组维度后,生成器不再只是“可迭代对象”,而是一种由调用方显式驱动的暂停执行对象。
30.3 yield from delegation
yield from 把一个子迭代器接到当前生成器的暂停执行链上。它的工作目标是让外部调用方像驱动当前生成器一样驱动子迭代器:普通产出值向外转发,send() 尽量向子生成器转发,throw() 和 close() 也按协议向下传递;子生成器结束时,StopIteration.value 成为 yield from 表达式的结果。这个语义由 PEP 380 定义,并从 Python 3.3 起进入语言。
下面的例子把委托关系压缩到最小形式。parent() 自己没有直接接收外部第一次发送的数据,它把恢复动作交给 child(),等 child 结束后再拿到 child 的返回值。
def child():
received = yield "child-ready"
return f"child-return:{received}"
def parent():
result = yield from child()
yield f"parent-sees:{result}"
p = parent()
first = next(p) # "child-ready"
second = p.send("data") # "parent-sees:child-return:data"
这段序列可以按 frame 关系追踪。next(p) 先进入 parent() 的 frame。parent() 执行到 yield from child(),创建并驱动 child generator。child 执行到 yield "child-ready",这个值穿过 yield from 返回给外部调用方。此时外部只持有 p,但实际挂起点位于 child 的 frame。
p.send("data") 再次恢复 parent generator 时,yield from 会把 "data" 送入 child。child 中的 received = yield "child-ready" 得到 "data",随后 return f"child-return:{received}" 结束 child。这个返回值通过 StopIteration.value 交给 yield from 表达式,parent 的 result 得到 "child-return:data",然后 parent 继续执行到自己的下一次 yield。
yield from 的工程价值在于保留暂停执行协议的完整性。手写 for value in child(): yield value 只能覆盖普通迭代产出;完整委托还要处理 send()、throw()、close()、子生成器返回值和异常传播。PEP 380 给出的是协议级委托模型,使拆分生成器逻辑时仍能保持外部驱动方式稳定。
异常和关闭在委托链上也有明确方向。外部对 parent 调用 throw() 时,yield from 会尝试把异常转发给当前子迭代器的 throw() 方法;子迭代器处理后继续产出的值仍向外返回。外部对 parent 调用 close() 时,委托链会优先关闭当前子迭代器,再让 parent 的清理路径继续收束。这组规则让资源清理能沿当前真实挂起点完成。
阅读 yield from 时,应先定位当前真实挂起对象。外层 generator object 仍是调用方入口;内层子迭代器可能才是保存当前 suspended frame 的对象。判断返回值时,应看子迭代器如何结束;判断异常时,应看异常先交给谁;判断关闭时,应看当前委托链最深处的对象是否有清理协议。
30.4 Frame persistence and lazy execution
惰性执行指生成器按需求推进 frame。函数调用创建 generator object,首次恢复才执行函数体;每次 yield 后,frame 停在当前位置;下一次恢复才继续消耗输入、计算下一个值或进入清理路径。这个模型让生成器适合流式处理,也会延长局部对象生命周期。
贯穿示例中的 total 展示了局部状态持久化。普通函数的局部变量在返回后通常随 frame 结束而脱离执行路径。生成器的局部变量在每次 yield 后继续保存在 suspended frame 中,直到生成器关闭、耗尽,或者外部引用消失并触发相应回收路径。total 在 next(g)、g.send(5)、g.send(None) 之间持续累加,依赖的就是这个 frame persistence。
惰性执行的另一个后果是输入错误和副作用可能延迟发生。生成器函数体内的文件打开、数据库查询、列表遍历、属性访问和用户代码调用,都发生在 frame 被恢复之后。只创建 generator object 通常只创建外部包装对象和必要的初始状态;真正的业务语句在恢复路径中执行。
def read_lazily(path):
print("function body starts")
with open(path, "r", encoding="utf-8") as handle:
for line in handle:
yield line.rstrip("\n")
lines = read_lazily("data.txt") # 此时函数体内 print 和 open 尚未发生
这段代码的工程含义是:创建 lines 时还没有打开文件;第一次 next(lines) 才会执行到 print() 和 open()。如果调用方创建了 generator object 却从未恢复它,函数体内的资源获取也不会发生。如果调用方恢复后停在 yield 处,with 管理的文件句柄会被 frame 持有,直到循环继续结束或生成器被关闭。
frame persistence 还会影响内存判断。只要 generator object 持有 suspended frame,frame 的 local namespace、临时中间对象和异常相关引用就可能继续保留。一个生成器在某次迭代中创建了大对象,并在 yield 后仍能从局部变量访问它,那么这个大对象会随 frame 一起存活。释放这类对象的稳定做法是让 frame 继续运行到局部引用被覆盖或函数结束,或者显式关闭生成器并断开外部引用。
def hold_buffer():
buffer = bytearray(10_000_000)
yield len(buffer)
buffer = None
yield "released"
这个例子中,第一次 yield 后 buffer 仍是局部变量绑定的大对象。调用方拿到长度值后,如果长期保存生成器并停在这个位置,buffer 也会继续被 frame 引用。恢复到 buffer = None 后,局部绑定改向 None,原对象才失去这条来自 frame 的引用路径。
惰性执行因此同时带来两个判断方向。性能上,它能把一次性计算拆成按需推进,降低峰值结果集合的占用。生命周期上,它会把 frame 和局部对象保留到下一次恢复或关闭。分析生成器内存问题时,应先问:当前 generator 是否 suspended;frame 停在哪个 yield;该位置之前哪些局部变量仍保持绑定;是否存在 finally、with 或委托链需要关闭。
30.5 Suspended execution boundaries
Suspended execution 可以用五个维度和普通调用、生成器、原生协程区分:调用结果、恢复入口、暂停点、返回值通道和调度责任。这组维度能把语法现象放回 runtime 对象,而不会把所有“会暂停”的对象混成一类。
普通函数调用直接进入 frame,并由当前调用栈连续推进。调用方拿到的是函数返回值或异常。生成器函数调用得到 generator object,调用方通过 iterator/generator 协议恢复它;暂停点是 yield 或 yield from;普通返回值通过 StopIteration.value 表达。原生协程函数调用得到 coroutine object,调用方通过 await、Task 或底层 coroutine 协议驱动它;暂停点是 await;调度通常交给 event loop 和 Task,这属于下一章的主线。
| 对象 | 调用时结果 | 恢复入口 | 暂停点 | 完成信号 |
|---|---|---|---|---|
| 普通函数 | 返回值或异常 | 直接调用 | 无跨调用方暂停点 | return 或异常 |
| 生成器函数 | generator object | next、send、throw、close | yield、yield from | StopIteration |
| 原生协程函数 | coroutine object | await、Task、底层 send | await | 返回值进入 await 结果或 Task 结果 |
这张表的重点是恢复责任。普通函数由当前调用栈负责执行到底;生成器由外部调用方一轮一轮驱动;协程通常由 event loop 调度 Task 来驱动。三者都可以关联 frame,但 frame 何时运行、何时暂停、谁负责恢复、返回值从哪里出来,差异很大。
生成器和 iterator 的边界也需要单独判断。所有 generator object 都是 iterator;许多 iterator 没有 suspended frame。list_iterator、dict_keyiterator 这类内置迭代器保存的是容器遍历位置和必要状态,调用 next() 只是推进内部索引或哈希表遍历状态。生成器保存的是 Python 代码 frame,可以在 yield 两侧执行任意用户代码、保留局部变量、接收 send() 值并处理注入异常。
生成器和 context manager 的边界来自标准库抽象。contextlib.contextmanager 可以把一个生成器包装成 context manager;包装后的用户入口是 with 协议,内部仍使用生成器的 yield 把进入阶段和退出阶段分隔开。阅读这类代码时,应同时看两层协议:外层 __enter__ / __exit__ 控制资源边界,内层 generator frame 保存进入阶段之后的执行位置和清理逻辑。
可复用检查顺序可以固定为五步。第一,确认调用结果是普通值、generator object、coroutine object 还是其它 iterator。第二,确认当前对象的恢复入口是直接调用、next()、send()、await、Task 调度还是 with 协议。第三,定位当前暂停点:yield、yield from、await 或容器迭代位置。第四,追踪值和异常的方向:向外产出、向内发送、向内注入、向外传播。第五,检查生命周期边界:frame 是否保留局部对象,委托链是否有子生成器,关闭路径是否能运行清理代码。
回到贯穿示例,g = accumulator() 的结果是 generator object;next(g) 和 send() 是恢复入口;incoming = yield total 是暂停点;total 向外产出,incoming 从外部进入;close() 触发 finally 收束资源和局部状态。用这组顺序读任何生成器,都能把表面上的“迭代”还原成对象、frame、协议和状态迁移之间的关系。
最小自检任务
阅读下面代码,判断 a、b 的值,并说明 outer()、child() 两个生成器在每一步的挂起位置和返回值传递路径。
def child():
received = yield "child-yield"
return received * 2
def outer():
total = yield from child()
yield total + 1
g = outer()
a = next(g)
b = g.send(10)
答案要点
a 的值是 "child-yield"。next(g) 先恢复 outer(),outer() 执行到 yield from child() 后创建并驱动 child generator。child() 执行到 yield "child-yield" 挂起,这个值穿过 yield from 返回给调用方。此时调用方只操作 g,真实挂起点在 child() 的 yield 表达式。
b 的值是 21。g.send(10) 恢复 outer() 时,yield from 把 10 转发给 child generator。child() 中的 received = yield "child-yield" 得到 10,随后 return received * 2 让 child 结束,并通过 StopIteration.value 把 20 交给 yield from 表达式。outer() 的 total 得到 20,继续执行到 yield total + 1,向外产出 21 并挂起。
执行到 b = g.send(10) 之后,child() 已经结束,outer() 挂起在 yield total + 1。如果再执行 next(g),outer() 会从这个 yield 后继续运行到函数末尾,调用方会收到 StopIteration。这个任务的核心判断是:yield from 期间外部入口仍是 outer generator object,当前真实暂停点和 send() 接收者可能位于子生成器。
本章知识点总结
- 生成器对象:包含
yield的函数调用会产生 generator object,函数体由后续恢复操作推进。 - 挂起帧:生成器在
yield处暂停时,frame 保留局部变量、指令位置和必要执行状态。 - 首次恢复:刚创建的生成器使用
next()或send(None)启动,因为此时尚无等待接收值的yield表达式。 - 双向 yield:
yield向外产出值,下一次send()的参数会成为该yield表达式的求值结果。 - 异常注入:
throw()在当前挂起点注入异常,生成器内部可以处理、继续产出或让异常传播。 - 关闭路径:
close()在挂起点触发GeneratorExit,使finally和资源清理路径得到执行机会。 - 委托执行:
yield from把外部驱动转发给子迭代器,并把子生成器结束时的返回值交给外层表达式。 - 真实挂起点:委托链中外部入口可能是外层 generator object,当前 suspended frame 可能位于子生成器。
- 惰性执行:生成器函数体按恢复需求推进,副作用、资源获取和异常触发时间会移动到恢复路径。
- 生命周期判断:suspended frame 会延长局部对象存活时间,内存与资源分析要定位当前
yield和仍然存在的局部绑定。 - 边界维度:普通函数、生成器和协程应按调用结果、恢复入口、暂停点、返回值通道和调度责任区分。