目 录CONTENT

文章目录

【Python】深入理解 Python yield:从迭代器到异步编程的演进

EulerBlind
2025-10-27 / 0 评论 / 0 点赞 / 2 阅读 / 0 字

引言

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 核心价值

  1. 双向通信:透明传递 send() 的值
  2. 异常传播:自动转发 throw() 的异常
  3. 返回值处理:捕获子生成器的 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 性能与适用场景对比

维度yieldsleep(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 实现异步?

  1. 已有的协程基础设施

    • yield 提供了现成的暂停/恢复机制
    • 不需要修改 Python 解释器核心
  2. 向后兼容

    • 可以与现有生成器代码共存
    • 渐进式演进路径
  3. 表达能力足够

    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 的核心职责

  1. 调度器:决定哪个协程运行
  2. I/O 多路复用:监听 socket、定时器等
  3. 回调管理:协程完成时通知依赖者

五、技术栈关系图

┌─────────────────────────────────────────┐
│          应用层                          │
│  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

七、总结

  1. yield 的本质:不仅是迭代器,更是协程机制的基础,通过栈帧保存/恢复实现控制流暂停

  2. yield from 的价值:处理完整的协程语义(双向通信、异常传播、返回值获取),为异步编程铺路

  3. 演进的必然性:从 yieldyield fromasync/await 是 Python 异步编程从混淆到清晰的过程

  4. 底层的统一性await 底层仍使用 yield from,但通过类型系统实现了语义分离

  5. 现代实践

    • 同步惰性计算 → 使用 yield
    • 生成器组合 → 使用 yield from
    • 异步 I/O → 使用 async/await

理解 yield 的演进历史,不仅能帮助我们深入掌握 Python 的协程机制,更能让我们在面对复杂的异步编程场景时游刃有余。

0
博主关闭了所有页面的评论