Python的生成器是语言中最容易被低估的特性之一。表面上看它只是用yield代替return的函数,实际上它实现了一套完整的执行状态管理机制:解释器会在yield处冻结整个函数调用栈,保存所有局部变量的值,等待外部驱动后再从断点精确恢复执行。理解这套暂停恢复机制,是掌握协程、异步编程和惰性求值的根基。本文将系统拆解生成器的状态流转过程,帮你把这块硬骨头彻底啃下来。

生成器与普通函数的本质区别
普通函数的执行模型是“一次性”的:调用它,解释器创建一个新的栈帧,函数从头执行到尾,return之后栈帧被销毁,所有局部变量随之消失。再次调用就是全新的一次执行,两次调用之间没有任何关联。这种模型简单直接,但有一个天然限制——函数无法在执行中途停下来把控制权交还给调用者。
生成器函数打破了这一限制。只要函数体内出现了一个yield关键字,Python在编译阶段就会把这个函数标记为生成器函数。此时直接调用它并不会执行任何函数体代码,而是返回一个生成器对象。函数体的执行被推迟到你显式地向生成器请求值的那一刻,这就是所谓的“惰性求值”。
def counter(n):
i = 0
while i < n:
yield i # 执行到这里暂停,把i交给外部
i += 1 # 恢复执行后从这里继续
gen = counter(3) # 此时函数体一行都没执行
print(gen) # <generator object counter at 0x...>
print(next(gen)) # 输出 0,函数执行到yield暂停
print(next(gen)) # 输出 1,从 i += 1 处恢复
print(next(gen)) # 输出 2
# 再次next会抛出 StopIteration关键在于:暂停时局部变量i的值被完整保存在生成器对象内部,恢复执行时无需重新初始化,直接沿用上次的值。这个特性让生成器天然适合处理大数据流——你不需要把一亿条数据全部装进内存,每次只算一条、交一条。
生成器的五种状态与流转规则
生成器对象在其生命周期内会经历几个明确的阶段,可以通过inspect.getgeneratorstate函数观察到。理解这些状态的名称和流转方向,是调试生成器问题的基础。
第一种是已创建状态(GEN_CREATED),即调用生成器函数之后、第一次next之前。此时函数体尚未执行任何一行代码。第二种是执行中状态(GEN_RUNNING),只在解释器内部请求值的一瞬间存在,单线程代码中几乎观察不到。第三种是挂起状态(GEN_SUSPENDED),即执行到yield处暂停等待,这是生成器生命周期中停留时间最长的状态。第四种是结束状态(GEN_CLOSED),函数正常执行完毕(return或执行到末尾)或者被close方法强制关闭后的归宿。
import inspect
def simple():
x = yield 10
yield x
g = simple()
print(inspect.getgeneratorstate(g)) # GEN_CREATED
next(g)
print(inspect.getgeneratorstate(g)) # GEN_SUSPENDED
next(g)
print(inspect.getgeneratorstate(g)) # GEN_CLOSED状态流转有几个重要规则需要牢记。已创建状态不能直接调用send发送非None值,因为函数还没执行到任何yield处,生成器不知道把这个值交给谁,会抛出TypeError,第一次必须用next(g)或g.send(None)来启动。已经结束的生成器再次next会抛出StopIteration,且该生成器无法复活,只能重新创建。这就是“生成器只能被消耗一次”的由来。
另一个容易踩的坑是:生成器对象被垃圾回收或显式close时,如果生成器内部有try/finally块,finally部分会被执行,GeneratorExit异常会在暂停点被抛出。利用这一点可以在生成器中做资源清理。
四种驱动方式:next、send、throw与close
生成器的暂停与恢复需要外部驱动,Python提供了四个方法。next是最基础的驱动方式,它恢复生成器执行,并把yield表达式的值取出来。可以把yield想象成一个双向通道:向左看它产出值给外部,向右看它接收外部传进来的值作为表达式的结果。
send是next的增强版,除了驱动执行,还能向生成器内部传值。yield表达式的求值结果就是send传入的参数,这是实现协程的核心机制——生成器不再是被动产出数据的一方,而是可以接收外部指令、根据输入调整行为的计算单元。
def echo():
while True:
received = yield
print('收到:', received)
g = echo()
next(g) # 启动生成器,执行到第一个yield暂停
g.send('hello') # 输出: 收到: hello
g.send('world') # 输出: 收到: worldthrow方法则是在暂停点向生成器内部注入一个异常。生成器恢复执行时,异常会在当前yield表达式处抛出,如果生成器内部没有捕获,异常会继续向外传播。这个方法常用于向协程发送控制信号,比如通知协程终止某项任务。
close方法用于提前终止一个处于挂起状态的生成器。它内部等价于在暂停点抛出GeneratorExit异常,生成器如果正常退出(捕获后不再yield),则关闭成功;如果生成器捕获异常后还试图再yield一个值,会触发RuntimeError。
def worker():
try:
while True:
yield '工作中'
finally:
print('清理资源,生成器被关闭')
w = worker()
print(next(w)) # 工作中
w.close() # 触发finally块,打印清理信息
# 此后再调用 next(w) 会抛出 StopIteration常见陷阱与实战应用场景
第一个常见陷阱是误以为生成器可以重复遍历。生成器是一次性可迭代对象,for循环遍历完之后再遍历会直接得到空结果,而不是报错,这往往让问题更隐蔽。如果需要多次遍历,要么把结果物化成列表,要么每次重新创建生成器。
def nums():
yield 1
yield 2
g = nums()
print(list(g)) # [1, 2]
print(list(g)) # [] 生成器已耗尽,静默返回空列表第二个陷阱是在生成器表达式中使用可变闭包变量,或者在一个生成器尚未耗尽时就依赖它的副作用。第三个陷阱是忘记生成器中的return值只能通过StopIteration异常的value属性获取,无法直接拿到。
在实战层面,生成器的状态管理机制有两个典型应用。一是协程式协作任务:利用send在多个生成器之间传递控制权和数据,实现轻量级的并发调度,早期框架就是靠yield实现的,现代的async/await语法在底层依然构建于同样的暂停恢复机制之上。二是管道式数据处理:把复杂的数据变换拆解成多个生成器串联,每条数据依次流过各个处理阶段,内存占用恒定,且每个阶段的逻辑独立、易于测试。
def read_lines(text):
for line in text.splitlines():
yield line.strip()
def filter_empty(lines):
for line in lines:
if line:
yield line
def to_upper(lines):
for line in lines:
yield line.upper()
# 串联成处理管道,惰性执行
pipeline = to_upper(filter_empty(read_lines(' a \n\n b \n')))
print(list(pipeline)) # ['A', 'B']掌握生成器的状态管理,不仅是学会一个语法特性,更是理解Python如何用栈帧冻结与恢复实现协程式控制流。当你下次面对async/await、上下文管理器或者惰性求值场景时,底层的这套机制会让一切都变得顺理成章。