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 把复杂的状态传递全包了。
生成器 → 协程演进关系 普通函数 跑到底就结束 生成器 yield 暂停/恢复 双向生成器 send() 传值 yield from 委托子生成器 原生协程 async/await 📌 演进路线 从普通函数 → yield 生成器(暂停/恢复)→ send 双向通信 → yield from 委托 → 最终演变为 async/await 原生协程。每个阶段都保留了前阶段的能力。

避坑指南

  • 别忘了预激:每次写 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 嘛。