for x in something 这行最普通的代码,背后是 Python 设计得相当精巧的一套协议。理解它,不只是为了"会写生成器",而是为了搞懂:为什么读 10GB 文件能不爆内存?yield 凭什么能让函数暂停又恢复?生成器和协程到底什么关系?这些问题都收束到两个核心:迭代器协议和生成器的状态机本质。
场景:for 循环到底在干什么
for 不是 C 那种带计数器的循环,它本质上是迭代器协议的客户端:
1 | for x in obj: |
这里有两个角色要分清。可迭代对象(Iterable) 实现 __iter__,能交出一个迭代器;迭代器(Iterator) 实现 __next__,每次吐一个值,没了就抛 StopIteration。list 是可迭代对象但不是迭代器(你不能对 list 直接 next()),它的 __iter__ 每次返回一个全新的迭代器——这就是为什么同一个 list 能嵌套 for 互不干扰。
机制:手写一个迭代器
1 | class Countdown: |
注意一个关键设计:迭代器把状态(self.n)和"取下一个"的逻辑(__next__)绑在一起。每次 next 推进一步状态。StopIteration 不是错误,而是协议约定的"终止信号"——for 会安静地捕获它。这也是为什么在生成器里 return value 会被包装成 StopIteration(value)。
生成器:编译器帮你写状态机
手写迭代器要维护 self.n 这种状态字段,繁琐。生成器让编译器替你做这件事——把含 yield 的函数自动变成一台状态机:
1 | def countdown(n): |
调用 countdown(3) 不执行函数体,而是立刻返回一个生成器对象。每次 next() 才执行到下一个 yield,在 yield 处冻结整个执行状态(局部变量、字节码指令指针都保存在生成器帧里),下次从那里继续。这就是"暂停/恢复"的真相:函数的栈帧没有被销毁,而是被挂起保存下来了。
懒求值是它最大的工程价值。处理大文件的经典写法:
1 | def read_lines(path): |
无论文件 10MB 还是 10GB,内存里始终只有当前这一行。这是生成器对比"先 readlines() 读进 list"的根本优势:空间复杂度从 O(n) 降到 O(1)。
yield from 与子生成器
yield from 不只是"展开子可迭代对象"的语法糖,它建立了一条透明通道:把外层 send、throw、返回值都正确地转发给子生成器。
1 | def chain(*iterables): |
在涉及 send 传值的协程场景里,手写 for ... yield 无法正确传递发送的值和异常,yield from 才是正解。这也是它被引入语言的真正动机。
从生成器到协程
yield 不仅能吐值,还能收值。gen.send(x) 会让上一个挂起的 yield 表达式的值变成 x,再继续执行:
1 | def accumulator(): |
这种"双向通信"的生成器就是早期 Python 协程的实现基础。后来的 async/await 在概念上一脉相承——await 之于事件循环,正如 yield 之于驱动它的 next/send,都是"把控制权交还给调度方并保存现场"。理解了生成器的暂停-恢复,再看 asyncio 就不再神秘。
工程权衡与边界
- 一次性。 生成器耗尽后再迭代直接结束,不会重来。需要多次遍历就别用生成器,或每次重新创建。这是 list 用不完的优势所在。
- 不能索引、不能 len。 生成器没有随机访问,
gen[3]、len(gen)都不行。要这些能力就得物化成 list,但那就放弃了省内存。 - 延迟执行带来的陷阱。 生成器体内的异常、副作用(如打开文件)要到真正迭代时才发生。
g = read_lines("nonexist")不报错,next(g)才报。调试时容易误判出错位置。 - 资源释放。 生成器没迭代完就被丢弃时,
with/finally里的清理依赖GeneratorExit——当生成器对象被回收,Python 会在挂起的yield处抛GeneratorExit触发finally。但如果你压住了这个机制(极少见),文件句柄可能晚释放。生产上建议显式close()或确保迭代到底。
小结
迭代器协议是 __iter__/__next__/StopIteration 三件套,是 for 循环的统一接口。生成器是编译器自动生成的迭代器状态机,用 yield 实现暂停与恢复,核心价值是惰性求值带来的 O(1) 空间。再往上,send/yield from 把生成器升级成可双向通信的协程,构成了 async/await 的思想源头。把"暂停时保存整个栈帧"这件事想明白,从大文件流式处理到异步编程的一大片地图就连成片了。