从yield 到await:Python 协程二十年的江湖变迁
如果你刚接触 Python 的异步编程,很容易以为目前的 async / await 天生就该如此优雅,仿佛一开始就应该这样写。可实际上,它们背后有一段漫长得让人“拍大腿”的成长史。

今天我们随手写的:
async def func():
await something()
实则是 Python 经过 将近二十年 才磨出来的成果。 这段进化旅程像极了武林秘籍的演化:从最初“木剑练习”的 yield,到民间高手自己改造的各种奇技,再到官方出手锤定标准,最终成为人人能用的 async/await。
许多人只会用,却没真正想过:为什么会有 yield?为什么后来又出现 yield from?为什么还要换成 async/await?它们有什么本质区别?
这篇文章我打算带你走一次时间线,把这段“协程江湖史”顺下来看。 不讲空理论,只讲这门技术是怎样被迫长大的。
如果你始终没弄懂它们之间的关系,看完你会超级清晰:
yield → send → 手写协程调度 → wrappertask(野生方案)→ yield from → async/await → asyncio
这是 Python 协程进化史的真实顺序。
而理解这条路线,你写异步代码的思维会彻底不一样。
一切的源头:yield
Python 最早引入 yield 的目的老老实实,就是为了节省内存,让你可以迭代一个大集合或者无限序列。但没人想到它会成为协程的起源。
当时它的典型用法超级朴素:
def counter():
i = 0
while True:
yield i
i += 1
gen = counter()
print(next(gen)) # 输出 0
print(next(gen)) # 输出 1
当年的 Python 设计者绝不敢想象,这个简单的小功能,后来会成为整个异步体系的逻辑基础。
它只是一个“能暂停的 return”。
但第一次惊喜,出目前 Python 2.5——send 出来了。
生成器突然变成了协程的雏形
send 的加入,让生成器拥有了“两个方向”的通道: 既能产出值,也能接收值。
def echoer():
while True:
msg = yield
print(f"Echo: {msg}")
e = echoer()
next(e)
e.send("Hi") # 打印 "Echo: Hi"
这一刻,Python 世界的人都意识到: 哎?这玩意儿怎么……有点协程那味儿了?
由于协程的定义是什么?
一个可以暂停、恢复、能与外部通讯的执行体。
而生成器突然间全部具备了。
但问题也随之而来: 如果协程要做复杂任务,会遇到一个“痛到不行”的问题——协程嵌套。
痛点开始显现:父协程如何调用子协程?
假设我们有:
- child_task:处理细分逻辑
- parent_task:调用它并统筹流程
def child_task():
for i in range(3):
print(f" Child step {i}")
yield f"data_{i}"
return "child_done"
def parent_task():
print("Parent start")
for data in child_task():
yield data # 手动转发
print("Parent end")
看起来没问题,但你仔细想就会发现 bug 一大堆。
1. send 无法透传
外部如果执行:
p.send("X")
这个“X”永远到不了 child_task。 由于 parent_task 根本没写接收逻辑。
2. throw 异常也传不进去
如果你向父任务抛异常,子任务完全不知道。
3. 子生成器的 return 回不来
child_task 的 return 值藏在 StopIteration.value,可父任务拿不到。
这意味着你想写一个“真正能代理子任务”的父协程,几乎不可能——除非你自己手动写一大堆中间逻辑。
开发者看到这里已经想掀桌子:
“这太折磨了吧?协程这么写,谁受得了?”
这时,一次改变 Python 进程的工程实践出现了:OpenStack 的 wrappertask。
一个真正改变语言发展方向的民间工法:@wrappertask
时间来到 2013 年。
那时 Python 3.3 已经推出了 yield from,但整个生产世界还在用 Python 2.7,没有人能升级。
OpenStack 这种世界级大型项目,却偏偏大量需要“协程嵌套的调度”。
无奈之下,他们做了件超级硬核的事:
自己模拟 yield from 的行为。
他们写了一个装饰器:@wrappertask。
只要你用上它,就可以这样写:
@wrappertask
def parent_task():
self.setup()
yield child_task()
self.cleanup()
这个 yield child_task() 的语义,与未来的:
result = yield from child_task()
惊人的接近。
wrappertask 到底做了什么?
以下是 Heat 项目中的真实实现(只是节选核心逻辑):
def wrappertask(task):
def wrapper(*args, **kwargs):
parent = task(*args, **kwargs)
for subtask in parent:
try:
if subtask is not None:
for step in subtask:
try:
yield step
except GeneratorExit as exit:
subtask.close()
raise exit
except:
try:
subtask.throw(*sys.exc_info())
except StopIteration:
break
else:
yield
except GeneratorExit as exit:
parent.close()
raise exit
except:
try:
parent.throw(*sys.exc_info())
except StopIteration:
break
return wrapper
看起来很复杂,但目的超级明确:
把父协程的控制权,完整代理给子协程。
包括:
- 子协程的产出值透传
- 子协程 throw 异常处理
- 子协程 close 管理
- 父子之间的异常双向通讯
是不是很像 yield from? 没错,它就是“土法炼钢版 yield from”。
而且该方案被大规模应用在真实系统里,证明超级可靠。
这段工程实践,本质上向 Python 官方传达了一个声音:
协程嵌套不是边缘需求,它是刚需。
终于,语言层面出手了:yield from 正式上线
2012 年,Python 3.3 推出了 yield from。
一句话总结它的作用:
自动完成协程嵌套中的所有代理逻辑。
换句话说:
你不用再手写中间转发,你也不用 wrappertask 这种补丁了。
你可以这么写:
def parent_task():
print("Parent start")
result = yield from child_task()
print(f"Child returned: {result}")
print("Parent end")
yield from 做的事情包括:
- 透传 send
- 透传 throw
- 捕获子协程 return
- 管理生命周期
- 简化父协程逻辑
所有之前令人头痛的中转逻辑,全透明解决。
协程组合真正变得优雅。
但这不是终点。
协程最大的问题不是语法,而是歧义
yield from 虽然强,但也隐含一个问题:
它太灵活了。
你可以在任何生成器里写 yield from —— 那它到底是用来做协程的?还是做迭代器的?还是做别的?
它没有上下文,没有类型,没有语义边界。
这让大型项目超级难维护。
于是 Python 决定做出一次彻底的升级:
让协程成为语言的“独立存在”。
协程从生成器体系中独立出来:async / await 登场
2015 年,Python 3.5 发布 async/await(PEP 492)。
这是一次高级别的“语义升级”。
async def child_coro():
await asyncio.sleep(1)
return "child_done"
async def parent_coro():
print("Parent start")
result = await child_coro()
print(f"Child returned: {result}")
两个关键变化:
1. async def 宣布:这是一个协程,不是生成器
生成器再也不会被误用在异步代码里。
2. await 宣布:我只能等待协程,不能乱用
去掉了 yield from 所有“非异步用途”的可能性。
这就像是抽象从“万能锤子”进化为“专业工具”。
你可以把 await 理解为“定制版 yield from”
它们之间的关系可概括为一句话:
await = 协程上下文下的 yield from
但 await 更专一、语义更清晰、工具链更完善。
这就是抽象升级的最终方向: 少即是多,明确比灵活更重大。
当 async/await 就位,整个生态拼图才完整:asyncio 出现了
event loop、task、future…… 这些概念过去都只能在各种三方库里“各写各的版本”。
而目前它们统一被整合进标准库:
import asyncio
async def main():
task1 = asyncio.create_task(some_work())
task2 = asyncio.create_task(other_work())
await task1
await task2
asyncio.run(main())
至此,Python 的异步模型终于成型:
- 明确的协程类型
- 清晰的 await 语义
- 统一的事件循环
- 完整的调度工具集
从 yield 到 async/await,这条路算是走通了。
回头看整段进化,你会发现一个很有意思的规律:
技术的演进永远不是“拍脑袋改变”, 而是现实逼出来的。
- yield 只是为了节省内存
- send 把协程的雏形打开了
- 协程嵌套太痛,于是社区自己造轮子(wrappertask)
- 真正的需求推动 yield from 成为语言标准
- 大型项目维护难度推动 async/await 出现
- 混乱的生态推动 asyncio 统一模型
每一步都是“逼出来”的。
而最终呈目前你眼前的,是经过无数人踩坑、无数项目打磨出来的成熟体系。
你目前轻松写的:
result = await work()
站在的,是整整二十年的技术沉淀。
写在最后:为什么你要了解这段历史?
由于你会突然发现:
理解技术的演化史,比学语法本身更重大。
当你懂:
- yield 为什么长成那样
- yield from 解决了什么
- await 为什么必要
- wrappertask 为什么是“工业化验证”
你写协程不会再凭感觉。
更重大的是,你会清楚一个实际:
任何看起来优雅的抽象,背后都经历过漫长的野蛮时代。
这句话不仅适用于 Python,也适用于技术世界的一切。















- 最新
- 最热
只看作者