← 返回Python进阶编程目录
← 返回学习笔记首页
专题: Python进阶编程系统学习
关键词: Python, yield from, 委托生成器, 子生成器, PEP 380, 协程, send
一、概述
yield from 是 Python 3.3(PEP 380)引入的生成器语法,用于在生成器函数中将迭代操作"委托"给另一个生成器(称为"子生成器")。它解决了一个长期存在的痛点:在生成器中手动迭代另一个可迭代对象时,需要编写样板代码来逐层转发值,同时无法正确处理 send()、throw()、close() 等生成器方法。
在引入 yield from 之前,要在一个生成器中迭代另一个可迭代对象,只能手动编写 for item in sub_iter: yield item。这种方式不仅繁琐,而且存在严重的功能缺陷——它无法将调用方通过 send() 发送的值正确传递给内层生成器。yield from 的诞生彻底改变了这一局面,它为生成器之间的双向通信提供了畅通无阻的通道。
PEP 380 :全称为 "Syntax for Delegating to a Subgenerator",由 Greg Ewing 提出,旨在简化生成器委托操作,并支持生成器返回值。该 PEP 于 Python 3.3 中被正式采纳。
前置知识 :学习本文之前,建议先了解 Python 生成器基础(yield 关键字)、生成器函数的执行流程,以及生成器的 send()/throw()/close() 方法。
二、基本语法与用法
yield from 的核心作用是"在一个生成器中委托另一个生成器进行迭代"。它的语法非常简洁:yield from <iterable>,其中 <iterable> 可以是任何可迭代对象,但最常见的是另一个生成器对象。
2.1 基本委托
下面是最简单的例子——一个生成器委托给另一个生成器来产生值:
def sub_gen ():
yield 'A'
yield 'B'
yield 'C'
def main_gen ():
yield 'start'
yield from sub_gen() # 委托给子生成器
yield 'end'
for item in main_gen():
print (item)
# 输出: start, A, B, C, end
可以看到,main_gen() 中通过 yield from sub_gen() 将迭代委托给了 sub_gen()。输出结果中,'A'、'B'、'C' 不是由 main_gen 直接 yield 的,而是由子生成器 sub_gen 产生的。
2.2 委托给任意可迭代对象
yield from 的右操作数不限于生成器,只要是可迭代对象即可:
def flatten_list (nested):
for sublist in nested:
yield from sublist # sublist 是列表,也是可迭代对象
nested = [[1 , 2 ], [3 , 4 , 5 ], [6 ]]
print (list (flatten_list(nested)))
# 输出: [1, 2, 3, 4, 5, 6]
核心机制 :yield from 会在当前生成器和子生成器之间建立一个"双向通道"。调用方通过 next()、send()、throw()、close() 对委托生成器的所有操作,都会被透明地转发给子生成器。
三、PEP 380 完整语义
理解 yield from 的真正威力,需要深入其底层语义。PEP 380 定义了 yield from 的完整行为,可以将其等价为以下伪代码:
RESULT = None # 保存子生成器的返回值
_i = iter (EXPR) # 获取子生成器迭代器
try :
_y = next (_i) # 预激子生成器
except StopIteration as _e:
RESULT = _e.value # 空迭代器时直接获取返回值
else :
while 1 :
_s = yield _y # 产出值并接收send值
try :
_y = _i.send(_s) # 转发给子生成器
except StopIteration as _e:
RESULT = _e.value # 子生成器结束,捕获返回值
break
RESULT # yield from 表达式的值
理解要点 :yield from 本质上是一个"透明代理"。它不断从子生成器获取值并 yield 给调用方,同时将调用方通过 send() 发送的值传递给子生成器。当子生成器抛出 StopIteration 时,其 value 属性(即 return 语句的值)成为 yield from 表达式的值。
3.1 异常与结束处理
完整的 PEP 380 语义还包括异常处理和生成器结束时的清理工作:
try :
_y = next (_i)
except StopIteration as _e:
RESULT = _e.value
else :
while 1 :
try :
_s = yield _y # 产出值并等待send
except GeneratorExit : # 处理close()
_i.close()
raise
except BaseException as _e: # 处理throw()
_y = _i.throw(_e)
else :
try :
if _s is None : # next() 调用路径
_y = next (_i)
else :
_y = _i.send(_s) # send() 调用路径
except StopIteration as _e:
RESULT = _e.value
break
重要提示 :yield from 会自动"预激"子生成器(即先调用一次 next())。这意味着不需要也不应该手动对子生成器调用 next() 或 send(None) 来进行预激活。
四、yield from vs 手动迭代子生成器
理解 yield from 的优势,最好的方法是将其与手动迭代方式进行对比。
4.1 代码简洁性对比
手动迭代(不推荐)
def chain_manual (*iterables):
for it in iterables:
for item in it:
yield item
# 无法处理 send()/throw()/close()
# 不能获取子迭代器的 return 值
yield from(推荐)
def chain_yield (*iterables):
for it in iterables:
yield from it
# 完全支持双向通信
# 可捕获子生成器返回值
# 异常处理自动传递
4.2 双向通信能力对比
这是两者最关键的差异。手动编写 for item in sub: yield item 时,调用方通过 send() 发送的值只能到达外层生成器,而外层的 yield item 只是一个产出表达式,接收到的值会被丢弃。而 yield from 会将这些值透明地转发给子生成器。
# 需要双向通信的场景:简易协程
def sub_coroutine ():
total = 0
count = 0
while True :
x = yield # 接收外部发送的值
if x is None :
break
total += x
count += 1
return (count, total) # 返回统计结果
def main_coroutine ():
result = yield from sub_coroutine()
print (f"统计结果: 共 {result[0]} 个数, 总和为 {result[1]}" )
# 创建并驱动协程
coro = main_coroutine()
next (coro) # 预激
coro.send(10 ) # → 透明转发到 sub_coroutine
coro.send(20 ) # → 透明转发到 sub_coroutine
coro.send(30 ) # → 透明转发到 sub_coroutine
try :
coro.send(None ) # 发送 None 让子生成器退出循环
except StopIteration :
pass
# 输出: 统计结果: 共 3 个数, 总和为 60
核心差异总结 :手动迭代只是单向的"拉取"数据;而 yield from 建立了双向通信通道,数据既可以"拉取"也可以"推送",同时异常传递和资源清理也自动处理。
五、send() / throw() / close() 的透明转发
这是 yield from 的最强大特性之一。当调用方在委托生成器上调用 send()、throw()、close() 时,这些调用会被透明地转发给子生成器。
5.1 send() 转发
def accumulator ():
total = 0
while True :
v = yield # 接收外部的值
if v is None :
return total
total += v
def delegator ():
# yield from 在此处充当透明代理
result = yield from accumulator()
print (f"子生成器返回: {result}" )
d = delegator()
next (d) # 预激,进入 accumulator
d.send(10 ) # → 通过 delegator 透明转发到 accumulator
d.send(20 ) # → 透明转发
d.send(30 ) # → 透明转发
try :
d.send(None ) # 发送 None 使 accumulator 退出并返回
except StopIteration :
pass
# 输出: 子生成器返回: 60
5.2 throw() 转发
当调用方在委托生成器上调用 throw() 时,异常会被转发到子生成器当前挂起的 yield 表达式处:
class ValueTooLarge (Exception ):
pass
def validator ():
while True :
try :
x = yield
if x > 100 :
raise ValueTooLarge (f"值 {x} 超过了上限 100" )
except ValueTooLarge as e:
print (f"验证器捕获: {e}" )
def proxy ():
yield from validator()
p = proxy()
next (p)
p.send(50 ) # 正常,通过
p.throw(ValueTooLarge , "测试异常" ) # 转发到 validator 的 yield 处
# 输出: 验证器捕获: 测试异常
5.3 close() 转发
当调用方在委托生成器上调用 close() 时,子生成器的 close() 也会被调用,从而确保子生成器中的 finally 块能够正常执行:
def resource_gen ():
try :
yield 'resource acquired'
yield 'working...'
finally :
print ('释放子生成器资源...' )
def wrapper ():
yield from resource_gen()
w = wrapper()
print (next (w)) # resource acquired
w.close() # → 触发子生成器的 finally
# 输出: 释放子生成器资源...
透明转发的意义 :在编写协程框架(如 asyncio 的旧版基于生成器的实现)时,委托生成器可以将所有协程通信无缝传递给底层协程,从而实现协程的组合与嵌套。
六、子生成器返回值捕获(StopIteration.value)
在 Python 3.3 之前,生成器函数中不能使用 return 语句返回值。PEP 380 改变了这一点——生成器中的 return X 等价于 raise StopIteration(X)。而 yield from 表达式可以捕获这个返回值。
6.1 基本用法
def data_producer ():
yield 1
yield 2
yield 3
return 'done' # 生成器返回值
def consumer ():
result = yield from data_producer()
print (f"子生成器返回: {result}" )
yield 'finished'
c = consumer()
print (list (c))
# 输出:
# 子生成器返回: done
# ['finished']
注意:return 'done' 在生成器中实际上会引发一个 StopIteration('done') 异常,这个异常值被 yield from 捕获并作为表达式的值。
6.2 获取返回值的底层机制
理解底层实现有助于深入掌握这一特性:
# 在 Python 解释器内部,生成器中的 return X 等价于:
raise StopIteration (X)
# 手动捕获生成器返回值(不使用 yield from):
def manual_capture ():
gen = data_producer()
try :
while True :
yield next (gen)
except StopIteration as e:
return e.value # 捕获返回值
# 而 yield from 在底层帮我们完成了这一切!
实践建议 :在设计生成器 API 时,可以将"过程数据"通过 yield 产出,将"最终结果"通过 return 返回。配合 yield from,外层生成器可以同时获取中间数据和最终结果,实现优雅的数据管道。
七、在递归生成器中的应用
yield from 在递归生成器中表现尤为出色,它能让我们以简洁优雅的方式遍历和操作嵌套数据结构。
7.1 深度优先遍历树结构
class TreeNode :
def __init__ (self , value, children=None ):
self .value = value
self .children = children or []
def depth_first (node):
yield node.value # 产出当前节点
for child in node.children:
yield from depth_first(child) # 递归遍历子节点
# 构建树: root → [child1 → [grandchild1, grandchild2], child2]
root = TreeNode('root' , [
TreeNode('child1' , [
TreeNode('grandchild1' ),
TreeNode('grandchild2' )
]),
TreeNode('child2' )
])
print (list (depth_first(root)))
# 输出: ['root', 'child1', 'grandchild1', 'grandchild2', 'child2']
7.2 完全扁平化嵌套列表
递归生成器最经典的应用之一是将任意深度的嵌套列表扁平化:
def deep_flatten (items):
for item in items:
if isinstance (item, (list , tuple , set )):
yield from deep_flatten(item) # 递归扁平化
else :
yield item
data = [1 , [2 , [3 , 4 ], 5 ], (6 , {7 , 8 }), 9 ]
print (list (deep_flatten(data)))
# 输出: [1, 2, 3, 4, 5, 6, 8, 7, 9]
递归生成器的威力 :如果没有 yield from,递归生成器的编写将变得非常笨拙——每层递归都需要手动编写 for x in recursive_call(...): yield x,代码臃肿且难以阅读。yield from 让递归生成器回归了自然的"直写"风格。
7.3 非递归结构的深度遍历
还可以使用 yield from 配合栈来替代递归,避免递归层数过深的问题:
def deep_flatten_iterative (items):
stack = [iter (items)]
while stack:
try :
item = next (stack[-1 ])
except StopIteration :
stack.pop()
continue
if isinstance (item, (list , tuple , set )):
stack.append(iter (item))
else :
yield item
八、生成器拼接与扁平化
yield from 提供了一种极其优雅的方式来拼接多个生成器或可迭代对象,比传统的 itertools.chain 更加直观。
8.1 拼接多个生成器
def gen_a ():
yield from 'ABC'
def gen_b ():
yield from 'DEF'
def gen_c ():
yield from 'GHI'
def concat ():
yield from gen_a()
yield from gen_b()
yield from gen_c()
print ('' .join(concat()))
# 输出: ABCDEFGHI
8.2 对比 itertools.chain
特性 yield from 拼接 itertools.chain
代码风格 声明式,自然直观 函数式,需显式传递参数
双向通信 支持 send/throw/close 不支持
子生成器返回值 可捕获 无法捕获
延迟计算 完全惰性 完全惰性
复杂度 需定义包装生成器 直接调用,一行搞定
# 使用 itertools.chain 实现同样功能
import itertools
print ('' .join(itertools.chain(gen_a(), gen_b(), gen_c())))
# 输出: ABCDEFGHI
# 但 chain 无法处理 send()
chained = itertools.chain(gen_a(), gen_b())
# chained.send(42) ← 这会抛出 AttributeError!
8.3 实战:读取多个文件的生成器
def read_files (*filepaths):
for fp in filepaths:
with open (fp, 'r' , encoding='utf-8' ) as f:
yield from f # 委托文件迭代器,逐行产出
# 使用示例
for line in read_files('file1.txt' , 'file2.txt' ):
print (line.strip())
性能优势 :yield from 委托子生成器时,值直接在子生成器和调用方之间传递,不需要经过委托生成器的 Python 层"中转",因此在某些场景下性能优于手动 for 循环迭代。
九、在简单协程中的应用
在 Python 3.5 引入 async/await 之前,yield from 是基于生成器的协程的核心实现机制。即使现在有了原生的协程语法,理解 yield from 在协程中的角色仍然有助于深入理解 Python 协程模型。
9.1 简单的事件驱动协程
class SimpleEventLoop :
def __init__ (self ):
self .tasks = []
def add_task (self , coro):
self .tasks.append(coro)
def run (self ):
while self .tasks:
coro = self .tasks.pop(0 )
try :
coro.send(None ) # 驱动协程一步
self .tasks.append(coro) # 放回队列
except StopIteration :
pass # 协程完成
def sleep (seconds):
# 模拟异步等待——基于生成的协程中表示"暂停"
yield from [] # 空迭代,立即返回
def task_a ():
for i in range (3 ):
print (f"任务A: 第 {i+1} 步" )
yield from sleep(1 )
def task_b ():
for i in range (3 ):
print (f"任务B: 第 {i+1} 步" )
yield from sleep(1 )
loop = SimpleEventLoop()
loop.add_task(task_a())
loop.add_task(task_b())
loop.run()
# 可能的输出(交替执行):
# 任务A: 第 1 步
# 任务B: 第 1 步
# 任务A: 第 2 步
# 任务B: 第 2 步
# 任务A: 第 3 步
# 任务B: 第 3 步
9.2 基于 yield from 的迷你 asyncio 模式
class Future :
def __init__ (self ):
self .result = None
self ._callbacks = []
def set_result (self , value):
self .result = value
for cb in self ._callbacks:
cb(self )
def add_done_callback (self , cb):
self ._callbacks.append(cb)
def __iter__ (self ):
yield self # 让事件循环可以获取这个 Future
return self .result # 返回最终结果
# 一个基于 yield from 的"协程"
def fetch_data (url):
future = Future()
# 模拟异步获取数据
import threading
def _fetch ():
import time
time.sleep(0.5 )
future.set_result(f"来自 {url} 的数据" )
threading.Thread(target=_fetch, daemon=True ).start()
# yield from 委托给 future,事件循环会在 Future 就绪时恢复协程
result = yield from future
return result
# 主协程使用 yield from 组合多个子协程
def main ():
data1 = yield from fetch_data('url1' )
print ("获取到第一个结果" )
data2 = yield from fetch_data('url2' )
print (f"最终数据: {data1}, {data2}" )
# 这段代码展示了 yield from 如何为 asyncio 的 await 语法奠定基础。
# 事实上,Python 3.5 的 async/await 语法在底层就是 yield from 的"语法糖"。
"yield from 是 Python 协程演进史上的关键里程碑——它打通了生成器之间的双向通信通道,为后续 async/await 语法的诞生铺平了道路。可以毫不夸张地说,没有 yield from,就没有今天 Python 中优雅的异步编程模型。"
现代最佳实践 :在 Python 3.5+ 中编写新协程应使用 async def/await 语法。但理解 yield from 仍然重要:① 维护旧代码(Python 3.3-3.4 的协程)时必不可少;② 深入理解 async/await 的实现原理;③ 某些高级生成器模式(如数据管道)仍然适用 yield from。
十、核心要点总结
yield from 的本质 :在调用方和子生成器之间建立透明的双向通信通道。调用方看到的是委托生成器,但实际交互的是子生成器。
特性 说明
基本语法 yield from <iterable>,委托给子生成器或任意可迭代对象
值转发 子生成器 yield 的值直接传递到调用方
send 转发 调用方 send 的值透明转发到子生成器
throw 转发 调用方 throw 的异常转发到子生成器
close 转发 调用方 close 时子生成器也关闭
返回值捕获 子生成器的 return 值可通过 yield from 表达式捕获
自动预激 yield from 自动 next() 子生成器,无需手动预激
递归支持 支持在递归生成器中直接用 yield from 调用自身
适用场景清单
生成器拼接 :将多个生成器的输出串联成一个连续的序列
嵌套数据结构遍历 :深度优先遍历树、图等递归数据结构
可迭代对象扁平化 :将任意深度的嵌套列表/元组扁平化
协程组合 :在基于生成器的协程中组合多个子协程
数据管道 :构建多阶段的数据处理流水线
资源委托 :将文件句柄、数据库游标等资源迭代委托给子生成器
注意事项
常见陷阱 :
不要对同一个生成器对象多次使用 yield from——生成器耗尽后无法重启
yield from 会预激子生成器,不要在 yield from 之前单独 next() 子生成器
如果不需要双向通信,简单的 for 循环 yield 或 itertools.chain 可能更合适
Python 3.5+ 的 async/await 中不要混用 yield from,应使用 await
一句话记忆 :yield from = "帮我从这个子生成器中取值,同时把我收到的所有消息也转发给它"。它是生成器世界中的透明代理模式。