文章目录
-
- 一、从一个委托问题开始
- [二、`yield from` 的基本形态](#二、
yield from的基本形态) - [三、`yield from` 的完整语义:PEP 380 拆解](#三、
yield from的完整语义:PEP 380 拆解) - [四、`yield from` 的三方通信模型](#四、
yield from的三方通信模型) -
- [4.1 `return` 值的捕获](#4.1
return值的捕获)
- [4.1 `return` 值的捕获](#4.1
- [五、双向通信:`send()` 的完整机制](#五、双向通信:
send()的完整机制) -
- [5.1 `send()` 的工作原理](#5.1
send()的工作原理) - [5.2 用装饰器自动启动](#5.2 用装饰器自动启动)
- [5.1 `send()` 的工作原理](#5.1
- [六、协程的历史脉络:从生成器到 async/await](#六、协程的历史脉络:从生成器到 async/await)
-
- 各阶段关键特征
- [为什么现在还要学 `yield from`?](#为什么现在还要学
yield from?)
- [七、`throw()` 与 `close()`:异常注入机制](#七、
throw()与close():异常注入机制) -
- [7.1 `throw()` 的用途](#7.1
throw()的用途) - [7.2 `GeneratorExit` 的传播规则](#7.2
GeneratorExit的传播规则)
- [7.1 `throw()` 的用途](#7.1
- [八、用 `yield from` 构建协程链](#八、用
yield from构建协程链) - [九、`yield from` 与递归数据结构](#九、
yield from与递归数据结构) - 十、实战:基于生成器协程的流量控制管道
- 十一、常见误区澄清
-
- [误区一:`yield from` 只是 `for...yield` 的简写](#误区一:
yield from只是for...yield的简写) - 误区二:协程和生成器是同一回事
- [误区三:`send()` 的第一次调用可以传值](#误区三:
send()的第一次调用可以传值)
- [误区一:`yield from` 只是 `for...yield` 的简写](#误区一:
- 十二、与前序文章的联系
- 小结
一、从一个委托问题开始
在 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
这段代码能工作,但存在两个隐蔽的问题:
send()值丢失 :如果调用方通过gen.send(x)向outer发送值,x永远到不了inner,因为for循环只处理__next__()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 def 和 await 语法糖出现,把协程的概念从生成器中独立出来,赋予专属语法。await expr 在语义上等价于 yield from expr,但只能在 async def 函数中使用,避免了生成器与协程的概念混淆。
为什么现在还要学 yield from?
- 维护遗留代码 :大量 Python 3.3~3.4 的代码库使用
@asyncio.coroutine+yield from - 深度理解 asyncio :
await是yield from的语法糖,事件循环的调度原理完全相同 - 构建不依赖 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 GeneratorExit块yield了一个值,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.value是yield from获取子生成器返回值的底层机制 - #06 生成器基础 :
yield的暂停/恢复机制、帧对象保存状态,是理解send()双向通信的前提
yield from 是 Python 并发编程历史上的关键一步。理解了它,asyncio 的 await 语法就不再是魔法,而是一个语义明确的委托机制。
小结
yield from的核心价值是三方透明通信 :send()、throw()、close()信号能无损穿透委托层到达子生成器- 子生成器的
return值成为yield from表达式的求值结果(通过StopIteration.value传递) - Python 协程历经 PEP 255 → 342 → 380 → 3156 → 492 五步演进,
async/await是yield from的专属语法糖 send()把生成器从单向生产者变成双向处理节点,第一次必须调用next()或send(None)启动throw()可向生成器注入任意异常,close()触发GeneratorExit,finally块保证执行
这篇文章写到这里,把
yield from和协程历史梳理清楚本就不容易------如果读完有收获,点个赞是最直接的鼓励。专栏"Python 进阶"持续更新,欢迎关注,和更多开发者一起把 Python 底层机制吃透。