从yield到await:Python协程演进

一个实际:今天我们写 async/await 的代码,背后是二十年一连串的折腾和妥协。直接说结论:从最早的 yield,到把生成器当协程,再到 wrappertask 这种工程解法,最终演化出 yield from,再到 async/await,整个过程并不是一蹴而就,而是被现实环境一步步推动的。

从yield到await:Python协程演进

把结论摆在前面,下面往回捋清楚这条路怎么走的。先看目前的形态,再倒着看各个阶段为什么会这么演进,细节放到每一步去讲清楚。

目前的样子:async/await + asyncio。语言层面有 async def,有 await,标准库里有事件循环(event loop)、Task、Future 这些东西可用,常见的异步框架也围绕这些东西构建。写异步逻辑比以前直观多了:一个协程可以直接 await 另一个协程,异常、返回值、控制流都被语言和运行时统一处理。这就是现代异步的“整洁”面貌。

从yield到await:Python协程演进

倒回去一点,到了 PEP 492、Python 3.5(2015 年)那会儿,语言把 await 作为专用语法加入。设计上把 await 当成在异步上下文里调用另一个“可等待对象”的专用方式,这样语义更明确、约束更清楚,也更容易被运行时优化。简单说,await 是把以前通用的 yield from 在异步场景里专门化,这换来的是更易读、行为更明确的代码。

在此之前,Python 通过 PEP 380 在 2012 年(Python 3.3)引入了 yield from。它解决了一个长期存在的问题:在一个生成器里面“调用”另一个生成器时,如何把所有的 send、throw、close 操作以及子生成器的返回值完整地转发过去。yield from 把这些细节标准化了,父生成器不再是简单的中间人,控制权能被完全委托给子生成器,异常会被正确传递,StopIteration 的 value 可以被捕获。这一改动让用生成器写协程风格的代码靠谱了许多。

从yield到await:Python协程演进

再往前倒。实际工程里,在 yield from 出现之前,许多大型项目早已遇到“生成器代理”这个痛点。举个典型的例子:OpenStack Heat 在其任务编排中,需要在一个父任务(列如 destroy)中启动并等待一个子任务(列如 delete),并且要求父子之间异常能传递、能 close。由于当时许多生产系统还运行在 Python 2.7 上,无法使用 yield from,项目组就自己实现了替代方案:@wrappertask 装饰器。这个装饰器的提交记录可以看到引入时间是 2013-05-07(commit
772f8b9e812ced3fe21425870bcde3b765cd73ab),后来在 2023-12-27 有移除记录(尽管这里的提交信息要看项目历史)。实务上,wrappertask 的目标就是尽可能模拟 yield from 的语义,让父生成器能够“启动”子生成器、把子生成器生成的值 yield 出去,并能把异常通过 .throw() 之类的方式在父子之间传播,还能响应 .close()。但它不是完全等价:wrappertask 没能做到 send 值的透明透传,也不能像 yield from 那样自然捕捉子生成器的 return 值(StopIteration.value)。换句话说,它是个工程级的权宜之计,但能把绝大多数编排需求撑起来。

再往前跳回到生成器本身。早在 Python 2.2(2001 年)引入 yield 的时候,yield 被当成一种类似增强的 return,用来写惰性序列、节约内存地遍历大数据。语义上它还只是“产出值”。到了 Python 2.5(2006 年),生成器的 API 加入了 .send(value) 方法。这个变化很关键:生成器不再只是向外产出值的函数了。通过 send,外部可以把数据送回生成器内部,生成器可以在自身的暂停点接收输入并继续执行。从功能上看,这让生成器更像一个可中断、可恢复、能与调用者互动的“独立执行体”,也就是协程的核心定义之一。

从yield到await:Python协程演进

有了 send,就有人尝试用生成器去模拟异步任务的组合。但随之出现的问题也超级明显。设想父生成器 yield 子生成器,这种写法表面上可行,但存在几个缺陷:外部对父生成器的 send 无法直接触达 child;如果外部在某点抛出异常,父生成器未必能把异常正确转给 child 去处理;当 child 用 return 返回某个值时,这个值被藏在 StopIteration.value 里,父层不容易直接拿到。这些细节在复杂的任务编排里都会爆发成难以调试的问题。

在语言层面还没给出合适语法前,工程师们做了不少创造性的补丁和库来弥补短板。OpenStack Heat 的 wrappertask 是其中最典型的一个案例。它通过装饰器包装生成器函数,使得父任务在执行时能把对 child 的 yield 当成真实的子任务挂起,父任务把子任务生成的数据再次 yield 给调度器,同时还实现了 throw/close 的传递逻辑。实现起来并不简单,代码里充满了对各种边界条件的处理。尽管它没有做到 send 值的透传或 return 值的自动捕获,但在当时的生产环境下,这已经是超级实用的解决方案。可以说,wrappertask 在没有语言支持的情况下取代了 yield from 的一部分职责。

从yield到await:Python协程演进

这类实践把真实生产问题反馈回语言设计者那里,形成了推动标准化的现实动因。PEP 380 的提出和采纳并非凭空的学术兴趣,而是基于广泛的需求:协程的组合、异常传播、返回值传递,这些都是日常开发里的基础需求,不是边缘问题。yield from 解决了 send/throw/return 等语义上的一致性问题,开发者们就能把更多精力放在业务逻辑上,而不是为生成器代理写各种补丁。

但 yield from 虽然解决了许多问题,它还是个通用的委托机制,不专门针对异步语义。后来 PEP 492 出现,把关注点收窄并专门化到异步场景。2015 年的 Python 3.5 通过 async/await 把协程的语法和语义提升到语言级别。await 不是把所有场景一网打尽的万能工具,它是为“可等待对象”设计的,语义更明确,约束更强,便于实现更高层的运行时支持。配合 asyncio 的成熟,Python 的异步栈终于有了完整的工具链:事件循环、Task、Future、以及语言层面的 async/await。

回到工程现实这一点上看,语言特性的出现往往领先于平台的普及。yield from 在 2012 年成为标准,但是大量生产环境还停留在 Python 2.7。这个时间差就催生出像 wrappertask 这样的工程方案。换句话说,并不是语言落后,更多是部署环境的落后。工程师不能等到理想状态才开始解决问题,他们需要立刻可用的替代方案。

在这里可以顺便说一句:许多语言特性的背后,都是大量的工程折中和临时方案堆出来的产物。看起来很优雅的 async/await,实则站在一堆早期实现和现实妥协的肩膀上。开发者日常面对的是兼容性、部署和稳定性,这些往往决定了工程方案的选择,而不是语言设计本身的美观度。

关于本文的作者和背景:笔者来自阿里云资源编排团队,团队专注于 IaC(基础设施即代码)自动化部署,核心服务以 Python 构建,负责海量云资源的高效、稳定编排。团队的工程实践直接面对生产环境约束,因此对生成器到协程的这些演进有直观感受。

另外顺带一提,我们也在把 AIGC 能力融合到产品里。团队的一个方案展示了如何利用自研的通义万相 AIGC 技术在 Web 服务中实现图像生成,包含文本到图像、涂鸦转换、人像风格重塑、人物写真等功能。对设计师和艺术家来说,这类工具能加速创作流程,低成本地试验视觉方案。感兴趣的可以看阿里云上关于基于通义万相快速构建 AI 绘画应用的技术解决方案页面。

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...