生成器进阶:`yield from`、协程历史与双向通信

文章目录

    • 一、从一个委托问题开始
    • [二、`yield from` 的基本形态](#二、yield from 的基本形态)
    • [三、`yield from` 的完整语义:PEP 380 拆解](#三、yield from 的完整语义:PEP 380 拆解)
    • [四、`yield from` 的三方通信模型](#四、yield from 的三方通信模型)
      • [4.1 `return` 值的捕获](#4.1 return 值的捕获)
    • [五、双向通信:`send()` 的完整机制](#五、双向通信:send() 的完整机制)
      • [5.1 `send()` 的工作原理](#5.1 send() 的工作原理)
      • [5.2 用装饰器自动启动](#5.2 用装饰器自动启动)
    • [六、协程的历史脉络:从生成器到 async/await](#六、协程的历史脉络:从生成器到 async/await)
      • 各阶段关键特征
      • [为什么现在还要学 `yield from`?](#为什么现在还要学 yield from?)
    • [七、`throw()` 与 `close()`:异常注入机制](#七、throw()close():异常注入机制)
      • [7.1 `throw()` 的用途](#7.1 throw() 的用途)
      • [7.2 `GeneratorExit` 的传播规则](#7.2 GeneratorExit 的传播规则)
    • [八、用 `yield from` 构建协程链](#八、用 yield from 构建协程链)
    • [九、`yield from` 与递归数据结构](#九、yield from 与递归数据结构)
    • 十、实战:基于生成器协程的流量控制管道
    • 十一、常见误区澄清
      • [误区一:`yield from` 只是 `for...yield` 的简写](#误区一:yield from 只是 for...yield 的简写)
      • 误区二:协程和生成器是同一回事
      • [误区三:`send()` 的第一次调用可以传值](#误区三:send() 的第一次调用可以传值)
    • 十二、与前序文章的联系
    • 小结

一、从一个委托问题开始

Python 进阶 #06 中,生成器流水线展示了把多个生成器串联的能力。但有一种更常见的需求:一个生成器想把一部分工作委托给另一个生成器------不只是把数据转发过去,而是让子生成器直接与最终调用方通信,就像中间层完全透明一样。

先看一个典型的"手动转发"写法:

python 复制代码
def inner():
    yield 1
    yield 2
    yield 3

def outer_manual():
    # 手动把 inner 的每个值转发出去
    for value in inner():
        yield value
    yield 4
    yield 5

这段代码能工作,但存在两个隐蔽的问题:

  1. send() 值丢失 :如果调用方通过 gen.send(x)outer 发送值,x 永远到不了 inner,因为 for 循环只处理 __next__()
  2. return 值丢失inner()return 值(附在 StopIteration.value 上)在 for 循环里被静默丢弃

这两个问题在简单场景下感知不到,但在双向通信(协程)场景中会造成严重的语义缺失。

yield from 就是为解决这两个问题而生的。


二、yield from 的基本形态

yield from <expr> 中的 <expr> 可以是任意可迭代对象:

python 复制代码
def chain(*iterables):
    for iterable in iterables:
        yield from iterable

list(chain([1, 2], "ab", range(3, 6)))
# [1, 2, 'a', 'b', 3, 4, 5]

对于简单的可迭代对象,yield from 等价于 for item in iterable: yield item。但当 <expr> 是另一个生成器时,它做的事情远不止转发值。


三、yield from 的完整语义:PEP 380 拆解

PEP 380(Python 3.3)用一段伪代码描述了 yield from 的完整语义。简化后的核心等价逻辑如下:

python 复制代码
# RESULT = yield from EXPR
# 等价于以下伪代码:

_i = iter(EXPR)             # 获取子生成器迭代器
_y = next(_i)               # 取第一个值
while True:
    try:
        _s = yield _y       # 把子生成器的值送给外部调用方,并等待外部发回值
    except GeneratorExit:
        _i.close()          # 外部调用 .close(),传给子生成器
        raise
    except BaseException as _e:
        # 外部调用 .throw(),传给子生成器
        try:
            _y = _i.throw(type(_e), _e, _e.__traceback__)
        except StopIteration as _e:
            RESULT = _e.value   # 子生成器 return 的值成为 yield from 的结果
            break
    else:
        # 外部发来的值转发给子生成器
        try:
            if _s is None:
                _y = next(_i)
            else:
                _y = _i.send(_s)
        except StopIteration as _e:
            RESULT = _e.value
            break

这段伪代码揭示了 yield from 与手动循环的本质差异:

行为 手动 for 循环 yield from
向子生成器转发 yield
向子生成器转发 send()
向子生成器转发 throw() 异常
向子生成器转发 close()
获取子生成器 return

四、yield from 的三方通信模型

next()/send(value)
透明转发
yield value
透明透传
throw(exc)
透明转发异常
StopIteration(return_val)
RESULT = return_val
外部调用方

(next / send / throw / close)
委托生成器

(含 yield from 的函数)
子生成器

(被 yield from 调用的函数)

委托生成器在 yield from 期间是完全透明的:外部调用方感知不到中间层的存在,所有信号(值、异常、关闭)都能无损地穿透委托层到达子生成器。

4.1 return 值的捕获

python 复制代码
def accumulator():
    """子生成器:接收数字,返回总和"""
    total = 0
    while True:
        value = yield          # 接收外部 send() 的值
        if value is None:
            break
        total += value
    return total               # return 值会被 yield from 捕获

def pipeline():
    """委托生成器"""
    result = yield from accumulator()   # result = accumulator 的 return 值
    print(f"accumulator 返回值:{result}")
    yield result               # 把结果再 yield 给最终调用方

gen = pipeline()
next(gen)          # 启动:推进到第一个 yield(在 accumulator 内部)
gen.send(10)       # 发送 10
gen.send(20)       # 发送 20
gen.send(30)       # 发送 30
try:
    gen.send(None) # 发送 None,触发 accumulator 的 break → return 60
except StopIteration as e:
    pass
# 输出:accumulator 返回值:60

五、双向通信:send() 的完整机制

生成器不只是数据的生产者 ,也可以是数据的消费者send() 方法让调用方能把值"注入"到生成器内部。

5.1 send() 的工作原理

yield 表达式有两个面向:

  • 向外 :暂停生成器,把 yield 右侧的值传给调用方
  • 向内 :当 send(value) 被调用时,yield 表达式的求值结果就是 value
python 复制代码
def echo_generator():
    """接收并返回每个发送来的值"""
    received = None
    while True:
        sent = yield received    # ← 这里既 yield 出去,又接收进来
        received = f"Echo: {sent}"

gen = echo_generator()
next(gen)              # 必须先 next() 启动,推进到第一个 yield
                       # 此时 yield 出去的是 None(received 初始值)

print(gen.send("hello"))   # Echo: hello
print(gen.send("world"))   # Echo: world
print(gen.send(42))        # Echo: 42

关键细节 :第一次必须调用 next(gen)gen.send(None) 来启动生成器,直接 gen.send("hello") 会抛出 TypeError: can't send non-None value to a just-started generator

5.2 用装饰器自动启动

每次手动调用 next() 启动生成器是个固定仪式,容易忘记,可以用装饰器消除:

python 复制代码
import functools

def auto_start(gen_func):
    """装饰器:自动启动生成器到第一个 yield"""
    @functools.wraps(gen_func)
    def wrapper(*args, **kwargs):
        gen = gen_func(*args, **kwargs)
        next(gen)   # 自动推进到第一个 yield
        return gen
    return wrapper

@auto_start
def averager():
    """持续接收数字,每次返回当前平均值"""
    total = 0.0
    count = 0
    average = None
    while True:
        value = yield average
        if value is None:
            break
        total += value
        count += 1
        average = total / count

avg = averager()  # 不需要手动 next()
print(avg.send(10))   # 10.0
print(avg.send(30))   # 20.0
print(avg.send(5))    # 15.0

六、协程的历史脉络:从生成器到 async/await

理解 yield from 为什么被设计成现在这样,需要了解 Python 协程的演化路径。
PEP 255

Python 2.2 (2001)

生成器基础

yield 单向产出值
PEP 342

Python 2.5 (2005)

生成器增强

send/throw/close

yield 变成表达式
PEP 380

Python 3.3 (2012)

yield from

子生成器委托

三方透明通信
PEP 3156

Python 3.4 (2014)

asyncio 标准库

@asyncio.coroutine

yield from Future
PEP 492

Python 3.5 (2015)

async/await 语法

原生协程独立语义

不再是生成器子集

各阶段关键特征

PEP 255(2001) :引入 yield 关键字,生成器只能单向产出值,不能接收外部输入。协程概念尚未出现在 Python 中。

PEP 342(2005) :这是最重要的转折点。yield 从语句变成表达式 ,有了返回值。新增 .send().throw().close() 三个方法。生成器从此可以充当协程------但每次启动仍需手动 next(),且没有委托机制。

PEP 380(2012)yield from 解决了协程委托问题,让编写异步代码栈成为可能。Twisted、Tornado 等框架开始基于生成器协程构建异步驱动。

PEP 3156(2014) :asyncio 进入标准库。@asyncio.coroutine 装饰的函数使用 yield from Future,这是第一次让生成器协程与事件循环正式结合。

PEP 492(2015)async defawait 语法糖出现,把协程的概念从生成器中独立出来,赋予专属语法。await expr 在语义上等价于 yield from expr,但只能在 async def 函数中使用,避免了生成器与协程的概念混淆。

为什么现在还要学 yield from

  1. 维护遗留代码 :大量 Python 3.3~3.4 的代码库使用 @asyncio.coroutine + yield from
  2. 深度理解 asyncioawaityield from 的语法糖,事件循环的调度原理完全相同
  3. 构建不依赖 asyncio 的协程系统:某些场景需要轻量级的协程调度,不需要完整的事件循环

七、throw()close():异常注入机制

7.1 throw() 的用途

gen.throw(ExcType, value=None, traceback=None) 在生成器挂起的位置注入一个异常,就像那个位置突然执行了 raise ExcType(value)

python 复制代码
def resilient_gen():
    """能从异常中恢复的生成器"""
    while True:
        try:
            value = yield
            print(f"收到: {value}")
        except ValueError as e:
            print(f"捕获 ValueError: {e},继续运行")
        except GeneratorExit:
            print("收到关闭信号,清理资源")
            return

gen = resilient_gen()
next(gen)                          # 启动
gen.send(42)                       # 收到: 42
gen.throw(ValueError, "非法输入")  # 捕获 ValueError: 非法输入,继续运行
gen.send(100)                      # 收到: 100
gen.close()                        # 收到关闭信号,清理资源

throw() 的典型工程用途:向生成器注入超时信号、取消信号,或者让生成器感知外部状态变化。

7.2 GeneratorExit 的传播规则

python 复制代码
def resource_gen():
    resource = acquire_resource()
    try:
        while True:
            yield process(resource)
    except GeneratorExit:
        release_resource(resource)  # 确保资源被释放
        raise                       # 必须重新抛出或直接 return
    finally:
        # finally 块在 GeneratorExit 时也会执行
        log_cleanup()

规则:

  • 生成器的 finally 块在 close()保证执行
  • 如果 except GeneratorExit 块执行了 return 或静默通过,生成器正常关闭
  • 如果 except GeneratorExityield 了一个值,Python 抛出 RuntimeError

八、用 yield from 构建协程链

下面是一个完整的协程链示例,模拟一个简单的任务调度场景:

python 复制代码
import time
from typing import Generator, Any

# -------- 子协程:执行具体任务 --------
def fetch_data(url: str) -> Generator[float, None, dict]:
    """
    模拟异步 HTTP 请求。
    yield 出去的是"需要等待的时间",
    模拟调度器看到这个值后暂停,等时间到了再恢复。
    """
    print(f"  [fetch_data] 开始请求 {url}")
    yield 0.1   # 告诉调度器:等 0.1 秒
    print(f"  [fetch_data] 收到响应 {url}")
    return {"url": url, "status": 200, "data": "..."}


def process_response(data: dict) -> Generator[float, None, str]:
    """模拟 CPU 密集型处理"""
    print(f"  [process_response] 处理 {data['url']}")
    yield 0.05  # 告诉调度器:等 0.05 秒
    return f"processed:{data['url']}"


# -------- 委托协程:组合多个子协程 --------
def handle_request(url: str) -> Generator[float, None, str]:
    """委托给 fetch_data 和 process_response"""
    data = yield from fetch_data(url)           # 等 fetch_data 完成
    result = yield from process_response(data)  # 等 process_response 完成
    return result


# -------- 极简调度器 --------
def run_to_completion(coro: Generator) -> Any:
    """
    驱动单个协程运行到结束。
    实际的 asyncio 事件循环原理与此类似,
    只是它同时管理成百上千个协程。
    """
    delay = None
    try:
        while True:
            if delay:
                time.sleep(delay)
            delay = coro.send(None)  # 推进协程,拿到 yield 出来的等待时间
    except StopIteration as e:
        return e.value  # 协程 return 的值

# 运行
result = run_to_completion(handle_request("https://api.example.com/users"))
print(f"\n最终结果: {result}")
# 输出:
#   [fetch_data] 开始请求 https://api.example.com/users
#   [fetch_data] 收到响应 https://api.example.com/users
#   [process_response] 处理 https://api.example.com/users
#
# 最终结果: processed:https://api.example.com/users

这个极简调度器正是 asyncio 事件循环的缩影:await 语句(即 yield from)把"需要等待"的意图表达出来,调度器决定什么时候恢复哪个协程。


九、yield from 与递归数据结构

yield from 天然适合递归遍历树形结构,无需手动维护栈:

python 复制代码
from typing import Union, Iterator

# 嵌套任意深度的树节点
TreeNode = Union[int, list]

def flatten_tree(tree: TreeNode) -> Iterator[int]:
    """
    递归展平嵌套树结构。
    yield from 让递归调用的每一层都直接把值传给最终调用方,
    无需手动收集和转发。
    """
    if isinstance(tree, list):
        for subtree in tree:
            yield from flatten_tree(subtree)   # 委托给递归调用
    else:
        yield tree   # 叶子节点直接产出

nested = [1, [2, [3, 4], 5], [6, 7], 8]
print(list(flatten_tree(nested)))
# [1, 2, 3, 4, 5, 6, 7, 8]

对比手动递归写法:

python 复制代码
def flatten_manual(tree):
    result = []
    if isinstance(tree, list):
        for subtree in tree:
            result.extend(flatten_manual(subtree))  # 每层都要构建临时列表
    else:
        result.append(tree)
    return result

yield from 版本:

  • 不需要构建任何中间列表,内存恒定
  • 调用栈深度与树深度一致,递归深度超过 sys.getrecursionlimit() 时同样会栈溢出(这是两者共同的限制)

十、实战:基于生成器协程的流量控制管道

下面是一个更接近生产环境的例子:用协程链实现数据流量控制,在不使用 asyncio 的前提下处理背压(backpressure)问题。

python 复制代码
import time
from collections import deque

def rate_limiter(target_coro, max_per_second: float):
    """
    限流协程:控制发往下游协程的速率。
    接收上游 send() 的数据,按频率转发给 target_coro。
    """
    interval = 1.0 / max_per_second
    last_sent = 0.0
    
    target_coro.send(None)  # 启动下游协程
    
    while True:
        item = yield               # 从上游接收数据
        
        # 计算需要等待的时间(令牌桶简化版)
        now = time.monotonic()
        elapsed = now - last_sent
        if elapsed < interval:
            time.sleep(interval - elapsed)
        
        last_sent = time.monotonic()
        target_coro.send(item)     # 转发给下游


def batch_writer(output_list: list, batch_size: int = 3):
    """
    批量写入协程:攒够一批再写。
    """
    batch = []
    try:
        while True:
            item = yield
            batch.append(item)
            if len(batch) >= batch_size:
                output_list.append(list(batch))
                print(f"  [batch_writer] 写入批次: {batch}")
                batch.clear()
    except GeneratorExit:
        if batch:   # 写入最后一个不完整的批次
            output_list.append(list(batch))
            print(f"  [batch_writer] 写入最后批次: {batch}")


# 组装管道
results = []
writer = batch_writer(results, batch_size=3)
writer.send(None)   # 启动

limiter = rate_limiter(writer, max_per_second=100)  # 限速 100条/秒

# 上游持续发送数据
events = [f"event_{i}" for i in range(8)]
for event in events:
    limiter.send(event)

limiter.close()  # 关闭时会自动关闭下游(通过 GeneratorExit 传播)
writer.close()

print(f"\n最终结果批次数: {len(results)}")

这个管道展示了协程作为数据消费者的典型模式:

  • 每个协程既接收数据(yield 作为表达式)又可以向下游转发
  • close() 信号沿管道传播,确保每个节点都能完成最后的清理工作

十一、常见误区澄清

误区一:yield from 只是 for...yield 的简写

python 复制代码
# 这两段代码不等价!
def delegator_wrong(gen):
    for v in gen:
        yield v      # send/throw/close 信号无法透传

def delegator_correct(gen):
    yield from gen   # 完整语义,所有信号透传

误区二:协程和生成器是同一回事

从语言机制上,Python 3.4 之前的协程确实是"特殊的生成器"。但 Python 3.5 之后,async def 定义的原生协程是独立类型,不能被 for 循环直接遍历,不能用 yield from(只能用 await)。

python 复制代码
import asyncio

async def native_coroutine():
    await asyncio.sleep(0.1)
    return 42

# async def 定义的是协程对象,不是生成器对象
coro = native_coroutine()
print(type(coro))           # <class 'coroutine'>
print(hasattr(coro, '__next__'))  # False!不是迭代器

误区三:send() 的第一次调用可以传值

python 复制代码
gen = some_generator()
gen.send("first")  # TypeError: can't send non-None value to a just-started generator

# 正确:第一次必须 send(None) 或 next()
gen.send(None)     # 等价于 next(gen)
gen.send("second") # 后续才能发送非 None 值

十二、与前序文章的联系

本文在 Python 进阶 #06 生成器基础之上,补全了生成器作为协程的完整能力。回顾一下这几篇的知识递进路径:

  • #03 装饰器基础 :理解 functools.wraps 等装饰器工具,这里用到了 @auto_start 装饰器自动启动协程
  • #05 迭代器协议 :迭代器的 StopIteration.valueyield from 获取子生成器返回值的底层机制
  • #06 生成器基础yield 的暂停/恢复机制、帧对象保存状态,是理解 send() 双向通信的前提

yield from 是 Python 并发编程历史上的关键一步。理解了它,asyncioawait 语法就不再是魔法,而是一个语义明确的委托机制。


小结

  • yield from 的核心价值是三方透明通信send()throw()close() 信号能无损穿透委托层到达子生成器
  • 子生成器的 return 值成为 yield from 表达式的求值结果(通过 StopIteration.value 传递)
  • Python 协程历经 PEP 255 → 342 → 380 → 3156 → 492 五步演进,async/awaityield from 的专属语法糖
  • send() 把生成器从单向生产者变成双向处理节点,第一次必须调用 next()send(None) 启动
  • throw() 可向生成器注入任意异常,close() 触发 GeneratorExitfinally 块保证执行

这篇文章写到这里,把 yield from 和协程历史梳理清楚本就不容易------如果读完有收获,点个赞是最直接的鼓励。专栏"Python 进阶"持续更新,欢迎关注,和更多开发者一起把 Python 底层机制吃透。

相关推荐
kyle~1 小时前
ROS2---消息过滤
开发语言·c++·机器人·ros2
xieliyu.1 小时前
Java手搓二叉树:基础遍历与核心操作全解析
java·开发语言·数据结构·学习
张二娃同学1 小时前
专栏第01篇_深度学习导论
人工智能·python·深度学习·cnn
雪度娃娃1 小时前
C++异步日志系统
开发语言·c++
xyq20241 小时前
SVN 提交操作详解
开发语言
源码之家2 小时前
计算机毕业设计:Python医疗数据分析可视化系统 Flask框架 随机森林 机器学习 疾病数据 智慧医疗 深度学习(建议收藏)✅
python·机器学习·信息可视化·数据分析·flask·课程设计
Halo_tjn2 小时前
基于异常处理机制 相关知识点
java·开发语言·算法
沐知全栈开发2 小时前
WebPages 对象
开发语言