Python 协程入门:生成器、yield、yield from
很多人都觉得协程是个高大上的概念,其实说白了,它的根儿就在生成器里。我记得刚入行那会儿,看到 yield 关键字就发怵,总觉得这玩意像个黑魔法。后来踩了几个坑才明白——生成器就是协程的前身,你搞懂了生成器,协程的大门就开了一半。
生成器是什么
普通函数你调一次,执行到底,返回结果。生成器不一样,它每次调用只跑一段,然后“挂起”等你下次要。嗯,你可以把它理解成一个可以暂停的函数。
怎么创建生成器?最简单的方法:在函数里用 yield 关键字。
def count_up_to(n):
i = 1
while i <= n:
yield i
i += 1
g = count_up_to(3)
print(next(g)) # 1
print(next(g)) # 2
print(next(g)) # 3
print(next(g)) # StopIteration
每调用一次 next(g),函数就从上次暂停的地方继续,跑到下一个 yield 再停。最后没有值可给了,抛出 StopIteration。
个人习惯:我一般用
for ... in 来遍历生成器,这样不用手动处理 StopIteration,代码更干净。
yield —— 暂停与恢复
yield 不只是往外扔值,它还能接收值。我当年第一次看到这个特性时,愣了好几秒——函数居然能“收数据”?
def echo():
while True:
received = yield
print(f'收到: {received}')
g = echo()
next(g) # 启动生成器,跑到 yield 处暂停
g.send('hello') # 打印 "收到: hello"
g.send('world') # 打印 "收到: world"
看到 send 了吗?它把值传进去,生成器内部用 yield 表达式的值接收。这种双向通信,就是协程的基础。
| 方法 | 作用 | 注意 |
|---|---|---|
next(g) | 推进到下一个 yield | 首次调用前要预激 |
g.send(value) | 推进并传入值 | 首次调用必须传 None |
g.throw(exc) | 在暂停处抛出异常 | 可用来关闭生成器 |
g.close() | 停止生成器 | 内部会抛出 GeneratorExit |
我曾经踩过的坑:忘记预激(第一次调用
send 前必须 next 或 send(None)),结果直接传非 None 值,报 TypeError: can't send non-None value to a just-started generator。后来我写了个装饰器自动预激,舒服多了。
yield from —— 简化委托
你想想看,如果有一个生成器要调用另一个生成器,传统写法要自己写循环 for x in sub_gen: yield x。这代码重复不说,还没法处理子生成器的 return 值。yield from 就是来解决这个问题的。
def sub_gen():
yield 1
yield 2
return 'done'
def main_gen():
result = yield from sub_gen()
print(f'子生成器返回: {result}')
for v in main_gen():
print(v) # 输出 1, 2, 然后打印 "子生成器返回: done"
yield from 把 main_gen 和 sub_gen 之间搭了一条“管道”——send 的值能直接透传,异常也能透传,最后还能拿到子生成器的 return 值。说白了,它让生成器委托变得优雅多了。
核心区别:yield 是手动暂停/恢复,yield from 是自动代理子生成器。yield from 把复杂的状态传递全包了。
避坑指南
- 别忘了预激:每次写
send前先next一下,或者用装饰器自动预激。我曾经因为忘预激调试了半小时…… - yield from 的返回值:子生成器 return 的值会被 yield from 表达式拿到,不是遍历出来的。很多人写
for x in yield from sub_gen:然后发现拿不到 return——嗯,那不对。 - 生成器是惰性的:它不会一次性把所有值算出来,适合处理大数据流。我做过一个日志分析的脚本,用生成器逐行读取百兆文件,内存几乎不涨。
- yield 和 return 不能并存:在同一个函数里用 yield 就变成了生成器,这时 return 不能带值,否则语法错误(Python 3.3 之前甚至不能写 return)。
总结一下:生成器是协程的“小号版”,yield 提供暂停/恢复,send 提供双向通信,yield from 提供委托机制。这些概念是理解 asyncio 的基石。下回你看到 async/await,心里就能想——哦,就是个升级版的 yield 嘛。