同一行代码两种命运从next(generate_even(10))看懂 Python 生成器的状态语义先看一段看起来自相矛盾的代码defgenerate_even(max_number):foriinrange(max_number):ifi%20:yieldiforiingenerate_even(10):print(i)# 0 2 4 6 8next(generate_even(10))# 0next(generate_even(10))# 0next(generate_even(10))# 0 ← 为什么永远不前进igenerate_even(10)next(i)# 0next(i)# 2next(i)# 4 ← 同一个函数、同样参数却前进了同一行next(generate_even(10))重复三遍永远得到0把它赋值给变量再next却乖乖地0, 2, 4往前走。这不是 bug也不是解释器的怪癖。它是 Python 生成器最核心、也最容易被忽略的一条语义生成器的执行状态属于对象不属于函数调用。而generate_even(10)每次调用都诞生一个崭新的对象。本文从这一行代码出发把生成器的状态模型、迭代器协议、七类实战坑位与五种重放方案一次讲清。一、第一层把三行代码改写成一句话就懂了next(generate_even(10))完全等价于ggenerate_even(10)# 1. 新建生成器对象函数体一行都没执行resultnext(g)# 2. 推进到第一个 yield拿到 0delg# 3. 临时对象失去引用被垃圾回收result# → 0每次都是造一个新的 → 推到第一个yield→ 扔掉。第一个yield永远产出0所以答案永远是0。而igenerate_even(10)# 造一次next(i)# 0 → 暂停在 yield 处记住range 跑到了 0next(i)# 2 → 从暂停点恢复继续跑next(i)# 4变量i指向同一个对象对象内部保存着执行到哪了于是能接着走。这里有个极具迷惑性的细节第 3 步的del完全是隐式的没人会写出来。那个被丢掉的生成器连个名字都没有它卡在 0这件事不留任何痕迹——这正是它容易被忽略的原因。一个必须先纠正的直觉初学者常把defyield的函数当成普通函数理解调用它 执行它。实际语义是写法含义generate_even(10)构造一个生成器对象函数体不执行next(g)/g.send(x)/for x in g驱动这个对象才真正执行函数体函数体执行到yield挂起把值交给调用方控制权返回再次驱动从挂起处原地恢复局部变量、循环位置全都在换句话说yield让一个函数拥有了暂停键。普通函数调用是一次跑完、栈帧销毁生成器是跑到一半、原地冻结、等待唤醒。二、第二层生成器是一台有四个状态的机器生成器不是惰性列表这么简单的比喻能覆盖的它是一台显式的状态机。用inspect可以直接观察importinspectdefgen(n):foriinrange(n):yieldi ggen(3)inspect.getgeneratorstate(g)# GEN_CREATED 已创建尚未启动next(g)inspect.getgeneratorstate(g)# GEN_SUSPENDED 暂停在 yield 处可恢复list(g)# 抽干inspect.getgeneratorstate(g)# GEN_CLOSED 已关闭永久终结完整状态转移next() / send(None) GEN_CREATED ──────────────────────► GEN_RUNNING ──yield──► GEN_SUSPENDED │ │ │ 函数返回 / 抛异常 │ next() / send() / throw() ▼ │ GEN_CLOSED ◄─────────────────┘实测输出Python 3.14initial: GEN_CREATED after next: GEN_SUSPENDED after drain: GEN_CLOSED gi_frame: None gi_running: False gi_code: gen注意g.gi_frame在关闭后变成None——栈帧被释放了。这也是生成器省内存的根源暂停时它只保留必要的帧而不是像普通函数那样把整个调用链压在栈上。四个状态对应四条铁律GEN_CREATED时函数体没跑连第一行都没有。所以生成器里的参数校验、日志、副作用都不会在调用时发生deff():print(我什么时候执行)yield1f()# 什么都不打印next(f())# 打印一次我什么时候执行next(f())# 又打印一次因为这是一个新对象GEN_CLOSED是不可逆的。抽干后next()抛StopIteration而且没有任何 API 能重置。要重来唯一办法是新建一个。GEN_RUNNING期间不能重复驱动。在生成器内部对自己的 another 引用调用next()会得到ValueError: generator already executing这是防止重入的保护。throw()/close()也是驱动方式。g.close()会在挂起点抛出GeneratorExitfinally块得以执行——这是生成器管理资源文件、连接、锁的基础。三、第三层iter(g) is g为真意味着什么ggenerate_even(10)iter(g)isg# True这个True是理解后面所有坑位的钥匙。Python 里有两个容易混淆的角色Iterable可迭代对象实现了__iter__可以被反复遍历。典型是list、tuple、dict、range。Iterator迭代器实现了__next__知道当前位置。一次性消耗品。for x in obj的实际流程是it iter(obj)然后不断next(it)。对于 listl[0,2,4]iter(l)isl# False —— 每次 iter() 都返回一个新的 list_iteratorlist(l),list(l)# ([0,2,4], [0,2,4]) 可以反复遍历对于生成器ggenerate_even(10)iter(g)isg# True —— 它的 __iter__ 就是 return self生成器对象本身既是 iterable 又是 iterator__iter__只是把自己原样返回。后果非常实际任何内部调用iter()后接着消费的代码传给它的生成器都会接着上一个人的进度继续而不是从头开始。ggenerate_even(5)sum(g)# 6 ← 0 2 4把它抽干了sum(g)# 0 ← 不是计算错误是真的空了sum(g)第二次返回0不报错、不警告。一个静默返回0的函数比一个抛异常的函数危险得多。四、坑位清单七个真实会踩的地方坑 1以为同样参数 同样对象# 反例defget_evens():returngenerate_even(10)next(get_evens())# 0next(get_evens())# 0又是新的修复需要跨调用保持进度就必须把对象存在变量、属性、或容器里。ggenerate_even(10)# 存住get_evenslambda:g# 或者干脆返回同一个坑 2耗尽后静默返回空defconsume(it):returnsum(it)ggenerate_even(10)consume(g)# 20consume(g)# 0 ← 静默错误修复在边界处把它物化成list或者约定本函数消耗传入的迭代器并在文档/类型上写明。也可以加一道自检defconsume(it):ititer(it)totalsum(it)iftotal0:# 只能报警不能区分真的和为 0和已耗尽...returntotal更可靠的做法是显式检查状态仅对生成器有效importinspectdefconsume(it):ifinspect.isgenerator(it)andinspect.getgeneratorstate(it)GEN_CLOSED:raiseValueError(这个生成器已经被耗尽了)returnsum(it)坑 3一个生成器喂给两个消费者ggenerate_even(10)headnext(g)# 0restlist(g)# [2, 4, 6, 8]如果消费者 A和消费者 B都以为自己在读完整数据就必然有一方读到残缺的序列。这在解析器、流式管道里非常常见把同一个生成器传给两个函数第二个拿到的是剩下的。修复要么在边界处物化成list再分发要么用itertools.tee显式分叉注意tee会缓存有内存代价importitertools a,bitertools.tee(generate_even(6))list(a)# [0, 2, 4]list(b)# [0, 2, 4] ← 两条管线各自看到完整序列坑 4生成器表达式同样是一次性的squares(x*xforxinrange(5))sum(squares)# 30sum(squares)# 0 ← 同一个坑但注意区别每次求值表达式都会新建一个生成器。next(xforxinrange(3))# 0next(xforxinrange(3))# 0 ← 和本文开头的 generate_even(10) 完全同构一句话记住(...)是值每次写它都是新对象把它赋给变量才是状态。顺便一个常被拿来说事的性能点sum(xforxinrange(5))# 生成器表达式逐个产出sum([xforxinrange(5)])# 列表推导式先建整个 list两者结果相同但前者峰值内存是 O(1)后者是 O(n)。在数据量大时这才是分水岭。坑 5把生成器塞进默认参数或模块级变量defprocess(itemsgenerate_even(10)):# 默认值只在 def 时求值一次returnsum(items)process()# 20process()# 0 ← 第二次调用就空了这比可变默认参数的经典坑更隐蔽list默认值是共享且可增长生成器默认值是共享且会耗尽。两者都源于默认参数在函数定义时求值一次。修复默认值用None占位。defprocess(itemsNone):ifitemsisNone:itemsgenerate_even(10)returnsum(items)坑 6忘了close()资源释放只能靠 GCdefread_lines(path):fopen(path)try:forlineinf:yieldline.strip()finally:f.close()# 只有生成器被正常抽干 / close() / 抛异常时才会执行如果调用方中途break就丢弃了生成器finally什么时候跑取决于垃圾回收时机——在 CPython 里通常很快引用计数归零但在 PyPy 等实现上可能延迟很久。别把资源释放押在 GC 上。修复用with包住消费者或显式g.close()或用contextlib.closinggread_lines(data.txt)try:next(g)finally:g.close()坑 7生成器里return的值去哪了defwith_return():yield1returndonerwith_return()next(r)# 1next(r)# 抛 StopIteration: 而它的 .value 是 done捕获try:next(r)exceptStopIterationassi:print(si.value)# done在普通for循环里这个返回值被完全忽略。要拿它得用yield fromdefwrapper():resultyieldfromwith_return()# result doneyieldresultyield fromPEP 380不只是语法糖它会把子生成器的send/throw/close与返回值都正确转发——是构建生成器管道的基础设施。五、五种重放方案需求不同方案不同。核心问题只有一个状态应该活多久存在哪里方案写法可重放内存适用场景工厂函数每次新建for i in generate_even(10):✅O(1)默认首选天然无状态显式存一个对象g generate_even(10); next(g)❌O(1)需要手动推进的游标式读取物化为 listlist(generate_even(10))✅O(n)需要多次遍历、需要索引/切片itertools.teea, b tee(gen(4))✅O(缓存)两条管线并行消费同一数据源自定义类实现__iter__见下✅O(1)要既能反复遍历又能保存状态classEvenNumbers:既可反复遍历又符合容器语义。def__init__(self,limit):self.limitlimitdef__iter__(self):foriinrange(self.limit):ifi%20:yieldi evensEvenNumbers(10)list(evens)# [0, 2, 4, 6, 8]list(evens)# [0, 2, 4, 6, 8] ← 每次 iter() 都是一台全新的状态机这就是list与生成器的根本分野list.__iter__返回新迭代器而生成器的__iter__返回self。该选哪个数据只过一次、且可能很大 → 生成器省内存数据要过多次、或要随机访问 →list既想惰性又想多次遍历 → 自定义类 __iter__想真正并行消费 →tee但先问一句能不能干脆物化一句话决策问自己这份数据要被消费几次。一次 → 生成器多次 → 能重放的东西。六、生成器真正的主场与边界适合生成器流式读取大文件 / 日志 / 网络响应内存恒定无限序列itertools.count()、自定义fib()天然不会越界管道式数据处理read → parse → filter → aggregate每级一个生成器递归结构遍历树、图代码比手写栈短得多不适合生成器需要len()、索引、切片、随机访问需要多次遍历且不愿缓存数据量小且结构固定——直接用 list可读性更好逻辑复杂到需要多处yield与外部状态耦合——此时用一个类更清晰还有一个容易被忽视的性能事实每次yield都伴随一次帧的挂起/恢复切换单次开销高于普通函数调用。如果一个数据集小到能放进内存生成器省下的内存换不回那点帧切换成本。惰性是有代价的只在数据大到不能一次装下或根本是无限的时才真正划算。七、自测题Q1.下面的输出是什么ggenerate_even(10)print(next(g))print(next(generate_even(10)))print(next(g))答案0、0、2。第 2 行造了一个临时生成器吸走了第一次 next对g毫无影响。Q2.any(x 100 for x in range(1000))里生成器被消费了多少次迭代答案101 次。any在第一个满足条件的元素处短路返回True生成器随即被丢弃——这正是惰性 短路的组合价值。Q3.为什么在生成器内部调用自己的close()会抛异常抛的是什么答案抛ValueError: generator already executing。因为那台状态机正处于GEN_RUNNING生成器不能在自己的执行过程中被再次驱动或关闭GeneratorExit无法注入到正在运行的帧上这是防重入保护的一部分。各类同步原语的RuntimeError: generator ignored GeneratorExit则是另一回事——那是finally里又yield了导致的。Q4.def f(): yield与def f(): return的关键区别是什么答案前者是生成器函数调用返回生成器对象函数体不执行后者是普通函数调用立即执行并返回None。函数体内只要出现任意一个yield整个函数就变成生成器函数语义完全不同。Q5.为什么next(generate_even(10))这种看起来毫无意义的写法值得警惕答案因为它通常出现在更隐蔽的形式里sum(self.get_items())、for x in self.build_pipeline():……只要生产和保存没有分开每次都在造新东西。理解这一点才能看懂为什么第二次调用返回空这类诡异 bug。八、速查表# 创建ggen(10)# GEN_CREATED函数体未执行g(xforxinrange(10))# 生成器表达式同样是一次性的# 驱动next(g)# 推进到下一个 yieldg.send(v)# 推进并传入值需先 next() 预热g.throw(Exc)# 在挂起点抛异常g.close()# 抛 GeneratorExit触发 finally# 观察inspect.getgeneratorstate(g)# GEN_CREATED / GEN_RUNNING / GEN_SUSPENDED / GEN_CLOSEDinspect.isgenerator(g)# True/Falseiter(g)isg# 恒为 True —— 它就是自己的迭代器# 复用list(g)# 物化可多次遍历O(n) 内存itertools.tee(g)# 分叉带缓存classFoo:def__iter__(self):...# 自定义可重放容器首选三句话总结gen(...)是造对象不是跑函数next()才跑。状态在对象上不在调用上。生成器是iter(g) is g的一次性迭代器耗尽后永久关闭无 API 可重置。想清楚数据会被消费几次一次用生成器多次就物化或实现__iter__。参考资料PEP 255 – Simple Generatorsyield的原始设计PEP 342 – Coroutines via Enhanced Generatorssend/throw/close生成器从迭代器升级为协程PEP 380 – Syntax for Delegating to a Subgeneratoryield from与返回值转发PEP 525 – Asynchronous Generatorsasync for/async yieldPython 官方文档Generator Types、Iterator Types、inspect.getgeneratorstate附本文所有输出的实测脚本Python 3.14.7 实测可直接运行importinspect,itertoolsdefgenerate_even(max_number):foriinrange(max_number):ifi%20:yieldi# 1. 现象foriingenerate_even(10):print(i,end )print()# 0 2 4 6 8print(next(generate_even(10)))# 0print(next(generate_even(10)))# 0ggenerate_even(10)print(next(g),next(g),next(g))# 0 2 4print(iter(g) is g -,iter(g)isg)# True# 2. 状态机hgenerate_even(3)print(inspect.getgeneratorstate(h))# GEN_CREATEDnext(h)print(inspect.getgeneratorstate(h))# GEN_SUSPENDEDlist(h)print(inspect.getgeneratorstate(h))# GEN_CLOSEDtry:next(h)exceptStopIteration:print(exhausted: StopIteration)# 永久耗尽# 3. 耗尽陷阱jgenerate_even(5)print(sum(j),sum(j))# 6 0