今天我们习以为常的 async/await,是 Python 异步编程的标准范式。但很少有人意识到,这个简洁优雅的语言结构并非凭空而来。
它是一段跨越二十年的技术演进成果——从最原始的生成器(generator)出发,历经社区实践中的“打补丁”阶段(如 @wrappertask),再到语言层面引入 yield from 和原生协程,最终形成了现代异步体系。
本文将按技术发展的真实时间线与逻辑脉络,带你完整还原这段历史:
为什么需要协程?嵌套任务如何调度?wrappertask 是谁的“替身”?await 究竟比 yield from 强在哪?
我们将一步步揭开 Python 协程从“手工轮子”走向语言级支持的全过程。
早在 Python 2.2(2001年),语言引入了 yield 关键字,用于创建惰性计算的序列:
def counter(): i = 0 while True: yield i i += 1
gen = counter()print(next(gen)) print(next(gen))
此时,yield 被视为一种增强版的 return,主要用于节省内存地遍历大数据集或无限序列。
1.2 双向通信:send() 方法的诞生(Python 2.5)
真正的转折点出现在 2006 年的 Python 2.5,加入了
.send(value) 方法,使外部可以向生成器内部传递数据:
def echoer(): while True: msg = yield print(f"Echo: {msg}")
e = echoer()next(e) e.send("Hi")
这一特性彻底改变了生成器的角色:
✅ 它不再只是“产出值”,而是能接收输入、维护状态、主动暂停的独立执行体。这正是“协程(Coroutine)”的核心定义——一个可中断、可恢复、能与调用方协作的过程。
随着异步模式在实践中应用加深,开发者开始尝试用生成器模拟复杂任务流。然而很快遇到了一个经典问题:如何在一个生成器中“调用”另一个生成器,并正确转发所有控制信号?
设想我们有两个生成器:
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")
虽然功能上看似可行,但这种写法存在严重缺陷。
1. 控制流不透明
外部无法直接向 child_task 发送数据:
p = parent_task()next(p)p.send("X")
2. 异常无法穿透
如果外部抛出异常:
父生成器根本不知道该如何处理——它只是一个中间转发者,不具备代理能力。
3. 返回值丢失
当子生成器以 return "child_done" 结束时
其返回值藏在
StopIteration.value 中,父层无法自然获取。
我们需要一种机制,能让父生成器把“控制权”完全交给子生成器,直到后者完成为止。理想语义包括:
我们希望这样写:
def parent_task(): print("Parent start") result = yield from child_task() print(f"Child returned: {result}") print("Parent end")
但遗憾的是,在 Python 3.3 之前,这是非法语法!
于是,在语言尚未提供支持的时代,工程师们只能自己动手,造出了各种“模拟方案”。
第三阶段:社区补丁|OpenStack 的 @wrappertask 装饰器
为了填补 yield from 缺失带来的空白,大型项目开始实现自定义的委托机制。其中最具代表性的是 OpenStack Heat 项目的 @wrappertask 装饰器
。它是对 yield from 语义的一次完整工程模拟。
🔗 20130507 引入 @wrappertask 的 commit
https://github.com/openstack/heat/commit/772f8b9e812ced3fe21425870bcde3b765cd73ab
🔗 20231227 移除 @wrappertask 的 commit
https://github.com/openstack/heat/commit/772f8b9e812ced3fe21425870bcde3b765cd73ab
✅ 数字上看,@wrappertask 是在 yield from 标准化之后才被加入 OpenStack 的。
但这并不削弱它的价值,反而凸显了一个关键事实:
⚠️ 尽管 yield from 已经成为标准,但绝大多数生产系统仍在使用 Python 2.7,而该语法对 Python 2 完全不可用。
因此,@wrappertask 不是一个“逆潮复古”,而是在现实约束下的必要替代方案。
它的核心目的是允许一个生成器函数通过 yield 直接“启动”另一个生成器作为子任务:
@wrappertaskdef parent_task(): self.setup() yield child_task() self.cleanup()
这与未来 y
ield from 的使用方式惊人相似。
在 Heat 中,一个典型的@wrappertask示例是在一个云资源的destroy父任务中调用delete子任务:
class Resource(status.ResourceStatus) @scheduler.wrappertask def destroy(self): """A task to delete the resource and remove it from the database.""" yield self.delete()
if self.id is None: return
try: resource_objects.Resource.delete(self.context, self.id) except exception.NotFound: pass
self.id = None
以下为 Heat 中的原始代码逻辑:
def wrappertask(task): """ Decorator for a task that needs to drive a subtask. This is essentially a replacement for the Python 3-only "yield from" keyword(PEP 380), using the "yield" keyword that is supported in Python 2. For example:: @wrappertask
def parent_task(self): self.setup() yield self.child_task() self.cleanup() """ 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
💡 它完整实现了:
- yield 出子任务的数据
- 父子任务间的异常传播.throw()close()
尽管实现复杂,且未实现 send 值透传、return 值捕获等功能,但它成功支撑了 Heat 项目中复杂的任务编排需求。
可以说,@wrappertask 是 yield from 在现实世界中的一次大规模工程验证。
正因为像 OpenStack 这样的重大项目长期面临“生成器代理”的痛点,开发者社区才更清楚地认识到:
“ 协程组合不是一个边缘场景,而是一个普遍存在的基础需求。”
也正是这些广泛的实践,为 PEP 380(即 yield from)提供了坚实的现实依据。
🔁 因此,“后来者居上”并不矛盾——@wrappertask 虽然晚于 yield from 出现,但在 Python 2 用户的实际生态中,它成了无数人最早接触的“类协程组合”机制。
第四阶段:语法标准化|yield from 登场(Python 3.3)
2012 年,随着 Python 3.3 发布,PEP 380 正式引入 yield from,一举解决了嵌套生成器问题:
def parent_task(): print("Parent start") result = yield from child_task() print(f"Child returned: {result}") print("Parent end")
相比 @wrappertask,yield from 的优势在于:
从此,开发者无需再造轮子。
第五阶段:专用抽象|原生协程登场(Python 3.5)
尽管 yield from 极大提升了表达力,但它仍有局限:
因此,PEP 492 (2015年,Python 3.5)提出了更高级的抽象:
5.1 新关键字:async def 与 await
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}") print("Parent end")
关键变化:
5.2 await 与 yield from 的关系
🔄
本质上,await 是 yield from 在异步上下文下的专用化版本——去除了通用性,换取更强的语义清晰度。
第六阶段:生态成型|asyncio 与事件循环整合
最后,配合 async/await 的语义升级,官方标准库 asyncio 成为现代异步编程的核心:
import asyncio
async def main(): task1 = asyncio.create_task(some_work()) task2 = asyncio.create_task(other_work())
await task1 await task2
asyncio.run(main()) # 启动事件循环
关键组件成熟:
EventLoop:统一调度所有协程;
Task:封装协程为并发单位;
Future:表示未完成的结果;
gather/wait:批量管理多个协程;
至此,Python 完成了从“生成器兼职协程”到“原生异步优先”的全面转型。
💡 注:@wrappertask 出现在 yield from 之后,反映了 语言先进但平台滞后 的典型工程现实 —— 新特性普及需要过渡周期。
1.痛点驱动创新@wrappertask
2.抽象逐层递进
3.清晰优于灵活
今天的 await 虽然只是一行关键字,但它背后站着从普通 yield 到协程理念成熟的全部历程。
理解这段历史,不仅能让我们写出更好的异步代码,更能明白:
💬 每一个优雅的 API,都曾经历过无数粗糙的原型与漫长的打磨。
团队介绍:
笔者来自阿里云资源编排团队。团队深耕IaC(基础设施即代码)自动化部署,核心服务由Python构建,支撑海量云资源高效、稳定编排。我们以工程卓越驱动云上自动化,助力企业敏捷交付,让IaC真正落地生产级规模。
本方案展示了如何利用自研的通义万相 AIGC 技术在 Web 服务中实现先进的图像生成。其中包括文本到图像、涂鸦转换、人像风格重塑以及人物写真创建等功能。这些能力可以加快艺术家和设计师的创作流程,提高创意效率。
点击阅读原文查看详情。