yield、yield from、await 到底啥关系?最通俗、最完整的一次讲解

从yield 到await:Python 协程二十年的江湖变迁

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

yield、yield from、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,也适用于技术世界的一切。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
想飞快乐星球的头像 - 鹿快
评论 共1条

请登录后发表评论