引言
yield 是 Python 中一个看似简单却极其强大的关键字。它不仅仅是创建生成器的语法糖,更是 Python 异步编程演进历程中的关键基石。本文将从底层实现、协程机制到异步编程的历史演变,全面剖析 yield 的本质。
一、yield 的底层实现机制
1.1 核心原理:协程与状态机
当函数中包含 yield 关键字时,Python 不会立即执行函数体,而是返回一个生成器对象。
字节码层面的特殊处理:
def simple_gen():
yield 1
yield 2
import dis
dis.dis(simple_gen)
关键字节码指令:
GEN_START: 标记生成器开始YIELD_VALUE: 暂停执行并返回值RESUME: 恢复执行
生成器对象的内部状态:
# 生成器维护的关键信息
gi_frame # 执行帧(保存局部变量、执行位置)
gi_code # 代码对象
gi_running # 是否正在运行
gi_yieldfrom # yield from 的目标(如果有)
执行流程示例:
def demo():
print("开始")
x = yield 1 # 第一次暂停
print(f"收到: {x}")
y = yield 2 # 第二次暂停
print(f"收到: {y}")
gen = demo() # 创建生成器,函数还未执行
next(gen) # 执行到第一个 yield,输出"开始",返回 1
gen.send(10) # x=10,执行到第二个 yield,输出"收到: 10"
gen.send(20) # y=20,函数结束,输出"收到: 20",抛出 StopIteration
关键机制:
- 栈帧保存:执行到
yield时保存当前栈帧(局部变量、指令指针) - 控制权转移:返回调用者,生成器进入挂起状态
- 恢复执行:调用
next()或send()时从上次暂停位置继续
1.2 yield 与迭代器的关系
生成器是迭代器的特殊实现,但两者有本质差异:
# 手动实现的迭代器
class ManualIterator:
def __init__(self):
self.n = 0
def __iter__(self):
return self
def __next__(self):
if self.n < 3:
result = self.n
self.n += 1
return result
raise StopIteration
# 使用 yield 的生成器
def generator():
n = 0
while n < 3:
yield n
n += 1
| 维度 | 手动迭代器 | 生成器 (yield) |
|---|---|---|
| 状态保存 | 手动维护实例变量 | 自动保存局部变量和执行位置 |
| 控制流 | 每次 __next__() 从头执行 | 从上次 yield 位置恢复 |
| 底层机制 | 普通方法调用 | 协程机制(栈帧挂起/恢复) |
| 双向通信 | 不支持 | 支持 send(), throw() |
核心区别:生成器不仅仅是迭代器,它是惰性计算 + 协程的结合体。
二、yield from:协程委托机制
2.1 不只是语法糖
yield from 在 Python 3.3 引入,表面上看是简化代码的语法糖:
# 看起来像简化版
def simple():
yield from range(3)
# 等价于
def manual():
for value in range(3):
yield value
但实际上,yield from 处理了完整的协程语义:
# 完整的等价实现(简化版)
def yield_from_expansion(sub_gen):
_i = iter(sub_gen)
try:
_y = next(_i)
except StopIteration as _e:
return _e.value # 获取返回值
while True:
try:
_s = yield _y # 转发值给调用者
except GeneratorExit:
# 处理关闭
if hasattr(_i, 'close'):
_i.close()
raise
except BaseException as _e:
# 转发异常
if hasattr(_i, 'throw'):
_y = _i.throw(_e)
else:
raise
else:
# 转发 send 值
_y = _i.send(_s)
2.2 核心价值
- 双向通信:透明传递
send()的值 - 异常传播:自动转发
throw()的异常 - 返回值处理:捕获子生成器的
return值
2.3 实用场景
场景 1:递归生成器
def flatten(nested_list):
for item in nested_list:
if isinstance(item, list):
yield from flatten(item) # 递归展开
else:
yield item
list(flatten([1, [2, [3, 4], 5]])) # [1, 2, 3, 4, 5]
场景 2:状态机与解析器
def parse_number():
"""解析数字,返回数值"""
digits = ''
while True:
char = yield
if char.isdigit():
digits += char
else:
break
return int(digits) # 返回解析结果
def tokenize():
"""词法分析器"""
tokens = []
while True:
char = yield
if char is None:
break
elif char.isdigit():
# 委托给子生成器,获取返回值
number = yield from parse_number()
tokens.append(('NUMBER', number))
return tokens
场景 3:递归计算结果汇总
def compute_directory_size(path):
"""递归计算目录大小"""
import os
total = 0
for entry in os.scandir(path):
if entry.is_file():
yield entry.name, entry.stat().st_size
total += entry.stat().st_size
elif entry.is_dir():
# 获取子目录的总大小
subdir_size = yield from compute_directory_size(entry.path)
total += subdir_size
return total # 返回当前目录总大小
三、协作式调度:yield 与 sleep(0) 的对比
3.1 协作式 vs 抢占式
协作式调度(Cooperative):任务需要主动让出控制权
yield:应用层协程切换asyncio.sleep(0):事件循环调度
抢占式调度(Preemptive):操作系统强制切换任务
time.sleep(0):触发 OS 线程调度器
3.2 场景对比
使用 yield(同步协作):
def task1():
print("Task 1-1")
yield
print("Task 1-2")
def task2():
print("Task 2")
yield
t1, t2 = task1(), task2()
next(t1) # Task 1-1
next(t2) # Task 2
next(t1) # Task 1-2
使用 sleep(0)(线程协作):
import time
def cpu_intensive_task():
for i in range(1000000):
_ = i * i
if i % 10000 == 0:
time.sleep(0) # 让出 CPU 给其他线程
使用 asyncio.sleep(0)(异步协作):
async def task1():
print("Task 1-1")
await asyncio.sleep(0) # 让出控制权给事件循环
print("Task 1-2")
async def task2():
print("Task 2")
# 执行顺序:Task 1-1 → Task 2 → Task 1-2
3.3 性能与适用场景对比
| 维度 | yield | sleep(0) | await asyncio.sleep(0) |
|---|---|---|---|
| 调度层级 | 应用层手动管理 | OS 线程调度 | 事件循环调度 |
| 开销 | 极低(函数调用级别) | 系统调用开销 | 事件循环开销 |
| 适用场景 | 同步数据流、管道 | 多线程公平性 | 异步 I/O 并发 |
四、从 yield 到 async/await:异步编程的演进
4.1 历史演进路径
生成器协程 (yield)
→ yield from (Python 3.3)
→ async/await (Python 3.5+)
Python 3.4 时代(asyncio 早期):
@asyncio.coroutine
def fetch_data():
response = yield from asyncio.http_get(url)
result = yield from asyncio.process(response)
return result
Python 3.5+ 时代:
async def fetch_data():
response = await asyncio.http_get(url)
result = await asyncio.process(response)
return result
4.2 为什么早期用 yield 实现异步?
-
已有的协程基础设施
- yield 提供了现成的暂停/恢复机制
- 不需要修改 Python 解释器核心
-
向后兼容
- 可以与现有生成器代码共存
- 渐进式演进路径
-
表达能力足够
def coroutine(): result = yield from sub_coroutine() # 等待子协程 return result # 返回值传递
4.3 async/await 的关键改进
1. 语义明确性
# yield 时代:语义模糊
@asyncio.coroutine
def ambiguous():
x = yield from something() # 异步等待?还是生成器委托?
# async/await:语义清晰
async def clear():
x = await something() # 明确是异步等待
2. 类型系统友好
from types import GeneratorType, CoroutineType
def old_coro():
yield from ...
async def new_coro():
await ...
isinstance(old_coro(), GeneratorType) # True
isinstance(new_coro(), CoroutineType) # True(不同类型)
3. 防止误用
# yield 容易误用
@asyncio.coroutine
def bad():
yield from sync_generator() # 可以编译,但语义错误
# async/await 强制检查
async def good():
await sync_generator() # TypeError: object is not awaitable
4.4 底层实现的惊人真相
await 底层仍然编译成 yield from,但增加了类型检查:
# await 的等价逻辑(简化)
def new_async():
coro = asyncio.sleep(1).__await__() # 返回特殊迭代器
result = yield from coro # 底层仍用 yield from!
return result
关键区别:await 只能用于实现了 __await__() 的对象:
class Awaitable:
def __await__(self):
yield from actual_coroutine()
await Awaitable() # ✅ 合法
await regular_generator() # ❌ TypeError
4.5 Event Loop 的角色
yield 时代:手动调度
def simple_event_loop(coros):
queue = list(coros)
while queue:
coro = queue.pop(0)
try:
coro.send(None) # 恢复协程
queue.append(coro) # 重新加入队列
except StopIteration:
pass # 协程完成
async/await 时代:专业调度器
class EventLoop:
def run_until_complete(self, coro):
task = Task(coro, loop=self)
while not task.done():
# 处理 I/O 事件(epoll/kqueue/IOCP)
self._run_once()
# 调度就绪的协程
for ready_task in self._ready:
ready_task.step()
Event Loop 的核心职责:
- 调度器:决定哪个协程运行
- I/O 多路复用:监听 socket、定时器等
- 回调管理:协程完成时通知依赖者
五、技术栈关系图
┌─────────────────────────────────────────┐
│ 应用层 │
│ yield → yield from → async/await │
└─────────────────┬───────────────────────┘
│
┌─────────────────▼───────────────────────┐
│ 协程机制(栈帧保存/恢复) │
│ generator.send() / coroutine.send() │
└─────────────────┬───────────────────────┘
│
┌─────────────────▼───────────────────────┐
│ Event Loop (asyncio) │
│ - 调度协程 │
│ - I/O 多路复用 (epoll/kqueue/IOCP) │
│ - 回调管理 │
└─────────────────┬───────────────────────┘
│
┌─────────────────▼───────────────────────┐
│ 操作系统 │
│ - 系统调用 (select/epoll) │
│ - 线程调度 │
└─────────────────────────────────────────┘
六、最佳实践与选择指南
6.1 何时使用 yield
✅ 适合场景:
- 惰性数据生成(处理大文件、数据流)
- 管道式数据处理
- 自定义迭代逻辑
def read_large_file(path):
with open(path) as f:
for line in f:
yield line.strip() # 逐行读取,内存友好
6.2 何时使用 yield from
✅ 适合场景:
- 递归生成器(树、图遍历)
- 解析器、状态机
- 需要获取子生成器返回值
def flatten(items):
for item in items:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
6.3 何时使用 async/await
✅ 适合场景:
- 异步 I/O 操作(网络请求、数据库查询)
- 并发任务处理
- 现代异步编程
async def fetch_all(urls):
tasks = [fetch(url) for url in urls]
return await asyncio.gather(*tasks)
6.4 三者的功能定位
# yield:同步迭代
def data_stream():
for item in database:
yield transform(item)
# yield from:生成器组合
def combined():
yield from generator1()
yield from generator2()
# async/await:异步并发
async def fetch():
data = await http_get(url)
return data
七、总结
-
yield 的本质:不仅是迭代器,更是协程机制的基础,通过栈帧保存/恢复实现控制流暂停
-
yield from 的价值:处理完整的协程语义(双向通信、异常传播、返回值获取),为异步编程铺路
-
演进的必然性:从
yield→yield from→async/await是 Python 异步编程从混淆到清晰的过程 -
底层的统一性:
await底层仍使用yield from,但通过类型系统实现了语义分离 -
现代实践:
- 同步惰性计算 → 使用
yield - 生成器组合 → 使用
yield from - 异步 I/O → 使用
async/await
- 同步惰性计算 → 使用
理解 yield 的演进历史,不仅能帮助我们深入掌握 Python 的协程机制,更能让我们在面对复杂的异步编程场景时游刃有余。