从 yield 到 await:Python 协程的进化史

简介: Python协程历经二十年演进,从生成器yield起步,经社区补丁如@wrappertask实践验证,最终在async/await语法中实现语言级支持,完成异步编程的标准化转型。

引言

今天我们习以为常的 async/await,是 Python 异步编程的标准范式。但很少有人意识到,这个简洁优雅的语言结构并非凭空而来。


它是一段跨越二十年的技术演进成果——从最原始的生成器(generator)出发,历经社区实践中的“打补丁”阶段(如 @wrappertask),再到语言层面引入 yield from 和原生协程,最终形成了现代异步体系。


本文将按技术发展的真实时间线与逻辑脉络,带你完整还原这段历史:

为什么需要协程?嵌套任务如何调度?wrappertask 是谁的“替身”?await 究竟比 yield from 强在哪?

我们将一步步揭开 Python 协程从“手工轮子”走向语言级支持的全过程。


第一阶段:生成器的本质|yield 的出现

1.1 什么是生成器?

早在 Python 2.2(2001年),语言引入了 yield 关键字,用于创建惰性计算的序列:

def counter():
    i = 0
    while True:
        yield i
        i += 1

gen = counter()
print(next(gen))  # 输出 0
print(next(gen))  # 输出 1

此时,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)         # 预激:运行到第一个 yield
e.send("Hi")    # 打印 "Echo: Hi"

这一特性彻底改变了生成器的角色:

✅ 它不再只是“产出值”,而是能接收输入、维护状态、主动暂停的独立执行体。这正是“协程(Coroutine)”的核心定义——一个可中断、可恢复、能与调用方协作的过程。


第二阶段:现实困境|嵌套生成器的控制流难题

随着异步模式在实践中应用加深,开发者开始尝试用生成器模拟复杂任务流。然而很快遇到了一个经典问题:如何在一个生成器中“调用”另一个生成器,并正确转发所有控制信号?

2.1 示例:父子任务关系

设想我们有两个生成器:

  • 子任务负责处理具体逻辑;
  • 父任务负责流程编排。
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")
  
    # 想要“调用” child_task 并将其产出透传出去
    for data in child_task():
        yield data  # 手动循环转发
  
    print("Parent end")

虽然功能上看似可行,但这种写法存在严重缺陷。

2.2 手动转发的三大痛点

1. 控制流不透明

外部无法直接向 child_task 发送数据:

p = parent_task()
next(p)
p.send("X")  # ❌ 实际发送给了父生成器,但父层没有接收逻辑!

2. 异常无法穿透

如果外部抛出异常:

p.throw(ValueError())

父生成器根本不知道该如何处理——它只是一个中间转发者,不具备代理能力。

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 的 commithttps://github.com/openstack/heat/commit/772f8b9e812ced3fe21425870bcde3b765cd73ab

🔗 20231227 移除 @wrappertask 的 commithttps://github.com/openstack/heat/commit/772f8b9e812ced3fe21425870bcde3b765cd73ab

3.1 出现时间与背景

  • yield from 正式发布:2012年9月(Python 3.3)
  • @wrappertask 首次引入:2013年5月7日

✅ 数字上看,@wrappertask 是在 yield from 标准化之后才被加入 OpenStack 的。

但这并不削弱它的价值,反而凸显了一个关键事实:

⚠️ 尽管 yield from 已经成为标准,但绝大多数生产系统仍在使用 Python 2.7,而该语法对 Python 2 完全不可用。

因此,@wrappertask 不是一个“逆潮复古”,而是在现实约束下的必要替代方案

3.2 wrappertask 的设计目标

它的核心目的是允许一个生成器函数通过 yield 直接“启动”另一个生成器作为子任务:

@wrappertask
def parent_task():
    self.setup()
    yield child_task()
    self.cleanup()

这与未来 yield from 的使用方式惊人相似。

在 Heat 中,一个典型的@wrappertask示例是在一个云资源的destroy父任务中调用delete子任务:


# commit_id: e649574d4751ffd8578f63787621c3a26d383a34

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:
            # Don't fail on deleteif the db entry has
            # not been created yet.
            pass

        self.id = None

3.3 实现原理简析

以下为 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:
                # 如果子任务不为None,则需要驱动执行它
                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())
                            # 如果子任务因StopIteration结束,则跳出循环
                            except StopIteration:
                                break
                else:
                    # 如果子任务为None,则简单地yield一个值
                    yield
            # 捕获生成器关闭异常
            except GeneratorExit as exit:
                # 关闭父任务并重新抛出异常
                parent.close()
                raise exit
            # 捕获其他所有异常
            except:
                try:
                    # 将异常传递给父任务处理
                    parent.throw(*sys.exc_info())
                # 如果父任务因StopIteration结束,则跳出循环
                except StopIteration:
                    break
    return wrapper

💡 它完整实现了:- yield 出子任务的数据- 父子任务间的异常传播.throw()close()

尽管实现复杂,且未实现 send 值透传、return 值捕获等功能,但它成功支撑了 Heat 项目中复杂的任务编排需求。

3.4 历史地位:一场“超前落地”的工业化验证

可以说,@wrappertaskyield 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")

相比 @wrappertaskyield 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")

关键变化:

  • async def 明确定义一个 原生协程函数
  • await 替代 yield from,只能用于 awaitable 对象;
  • 类型清晰、上下文明确、安全性高;

5.2 await 与 yield from 的关系

🔄 本质上,awaityield 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

  • 虽是临时补丁,但它来源于 OpenStack 这类超大规模系统的实际调度需求。正是这些真实世界的挑战,反向推动了语言标准的演进。

2.抽象逐层递进

  • 技术演进不是跳跃式的,而是遵循 “hack → 模式 → 库 → 语法 → 生态” 的升维路径。

3.清晰优于灵活

  • async/await 牺牲了 yield from 的通用性,换来的是更低的认知成本和更高的工程可靠性。

今天的 await 虽然只是一行关键字,但它背后站着从普通 yield 到协程理念成熟的全部历程。

理解这段历史,不仅能让我们写出更好的异步代码,更能明白:

💬 每一个优雅的 API,都曾经历过无数粗糙的原型与漫长的打磨。

团队介绍:

笔者来自阿里云资源编排团队。团队深耕IaC(基础设施即代码)自动化部署,核心服务由Python构建,支撑海量云资源高效、稳定编排。我们以工程卓越驱动云上自动化,助力企业敏捷交付,让IaC真正落地生产级规模。



来源  |  阿里云开发者公众号

作者  |  剑洁

相关文章
|
1月前
|
数据采集 SQL 人工智能
评估工程正成为下一轮 Agent 演进的重点
AI系统因不确定性需重构评估体系,评估工程正从人工经验走向自动化。通过LLM-as-a-Judge、奖励模型与云监控2.0等技术,实现对Agent输出的可量化、可追溯、闭环优化的全周期评估,构建AI质量护城河。(238字)
|
1月前
|
人工智能 JSON Java
AI时代,我们为何重写规则引擎?—— QLExpress4 重构之路
AI时代下,规则引擎的需求反而更旺盛。QLExpress4 通过全面重构,在性能、可观测性和AI友好性上大幅提升。
616 15
AI时代,我们为何重写规则引擎?—— QLExpress4 重构之路
|
人工智能 Java 测试技术
代码采纳率如何提升至50%?AI 自动编写单元测试实践总结
借助Aone Copilot Agent,通过标准化Prompt指导AI生成单元测试代码,实现50%代码采纳率,显著提升测试效率与质量,推动团队智能化研发转型。
330 20
|
27天前
|
安全 Java Android开发
深度解析 Android 崩溃捕获原理及从崩溃到归因的闭环实践
崩溃堆栈全是 a.b.c?Native 错误查不到行号?本文详解 Android 崩溃采集全链路原理,教你如何把“天书”变“说明书”。RUM SDK 已支持一键接入。
817 226
|
17天前
|
监控 应用服务中间件 nginx
Agentic 时代必备技能:手把手为 Dify 应用构建全链路可观测系统
本文讲述 Dify 平台在 Agentic 应用开发中面临的可观测性挑战,从开发者与运维方双重视角出发,系统分析了当前 Dify 可观测能力的现状、局限与改进方向。
315 45
|
17天前
|
机器学习/深度学习 人工智能 缓存
让AI评测AI:构建智能客服的自动化运营Agent体系
大模型推动客服智能化演进,从规则引擎到RAG,再到AI原生智能体。通过构建“评估-诊断-优化”闭环的运营Agent,实现对话效果自动化评测与持续优化,显著提升服务质量和效率。
464 19
让AI评测AI:构建智能客服的自动化运营Agent体系
|
2月前
|
存储 调度 C++
16 倍性能提升,成本降低 98%! 解读 SLS 向量索引架构升级改造
大规模数据如何进行语义检索? 当前 SLS 已经支持一站式的语义检索功能,能够用于 RAG、Memory、语义聚类、多模态数据等各种场景的应用。本文分享了 SLS 在语义检索功能上,对模型推理和部署、构建流水线等流程的优化,最终带给用户更高性能和更低成本的针对大规模数据的语义索引功能。
318 25
|
1月前
|
运维 监控 数据可视化
故障发现提速 80%,运维成本降 40%:魔方文娱的可观测升级之路
魔方文娱集团携手阿里云构建全链路可观测体系,突破高并发场景下监控盲区、告警风暴等难题,实现故障发现效率提升80%、运维成本降低40%,推动运维从被动响应向智能预防演进。
130 10
故障发现提速 80%,运维成本降 40%:魔方文娱的可观测升级之路
|
27天前
|
机器学习/深度学习 人工智能 算法
PAIFuser:面向图像视频的训练推理加速框架
阿里云PAI推出PAIFuser框架,专为视频生成模型设计,通过模型并行、量化优化、稀疏运算等技术,显著提升DiT架构的训练与推理效率。实测显示,推理耗时最高降低82.96%,训练时间减少28.13%,助力高效低成本AI视频生成。
194 22
|
27天前
|
SQL JSON 分布式计算
【跨国数仓迁移最佳实践6】MaxCompute SQL语法及函数功能增强,10万条SQL转写顺利迁移
本系列文章将围绕东南亚头部科技集团的真实迁移历程展开,逐步拆解 BigQuery 迁移至 MaxCompute 过程中的关键挑战与技术创新。本篇为第六篇,MaxCompute SQL语法及函数功能增强。 注:客户背景为东南亚头部科技集团,文中用 GoTerra 表示。
238 20

热门文章

最新文章