【Python】深入浅出 Python yield 关键字与生成器底层原理

admin / Python / 已更新 2026-09-28 / 1834 字 · 约 15 分钟 / 671 次阅读 /

一、迭代体系核心概念透视

在深入掌握 yield 关键字前,必须清晰辨析 Python 中极易混淆的四个基础概念:可迭代对象(Iterable)、迭代器(Iterator)、迭代器协议(Iterator Protocol) 与 生成器(Generator)。

+-------------------------------------------------------------------------------+
| 可迭代对象 (Iterable: list, dict, str...)                                     |
| [拥有 __iter__() 方法,调用后产出迭代器]                                       |
+-------------------------------------------------------------------------------+
                                    │ 调用 iter(obj)
                                    ▼
+-------------------------------------------------------------------------------+
| 迭代器 (Iterator)                                                             |
| [拥有 __next__() 与 __iter__(),不断获取下一个元素,耗尽时抛出 StopIteration]   |
+-------------------------------------------------------------------------------+
                                    ▲
                                    │ yield 定义产出
+-------------------------------------------------------------------------------+
| 生成器 (Generator)                                                            |
| [特殊的迭代器!含 yield 语句,支持暂停/恢复,惰性求值,单向遍历,内存开销恒定为 O(1)]  |
+-------------------------------------------------------------------------------+

迭代体系核心概念横向对照

概念角色 本质定位 必须实现的方法 典型代表 核心特征 / 优势
可迭代对象 (Iterable) 存放数据的容器集合 __iter__() 或 __getitem__() list, tuple, dict, set, str 可被 for 循环重复多次遍历,数据全部预存内存中
迭代器 (Iterator) 产生数据的有状态数据流 __iter__() + __next__() iter([1, 2]), 文件句柄 open() 一次性遍历,无法回退重置,通过 next() 按需驱动
生成器 (Generator) 特殊迭代器(惰性计算流) __iter__() + __next__() + send() (x for x in range(10)), def gen(): yield 惰性求值(Lazy Evaluation),仅在需要时计算,内存占用 $O(1)$

二、Python for 循环的底层执行协议

Python 的 for 循环语法糖并不是直接在列表或容器上索引扫描,而是严格遵循迭代器协议(Iterator Protocol):

# 日常业务代码中的 for 循环
for item in [1, 2, 3]:
    print(item)

其底层在 CPython 解释器中的实际等价执行逻辑如下:

# 底层迭代器协议执行过程
iterator = iter([1, 2, 3])  # 1. 隐式调用 [1, 2, 3].__iter__() 获取专属迭代器
while True:
    try:
        item = next(iterator)  # 2. 循环调用 iterator.__next__() 依次获取下一个值
        print(item)            # 3. 执行循环体业务逻辑
    except StopIteration:      # 4. 捕获耗尽信号,优雅退出循环
        break

三、生成器函数与 yield 运行原理

1. 普通函数 vs 生成器函数

只要一个函数的代码块中包含 yield 关键字,CPython 编译器就会将其标记为 generator function(生成器函数),而非普通函数:

def normal_func():
    return 1

def gen_func():
    yield 1

print(type(normal_func))    # <class 'function'>
print(type(gen_func))       # <class 'function'>

print(type(normal_func()))  # <class 'int'> -> 立即执行并返回结果
print(type(gen_func()))     # <class 'generator'> -> 不执行内部代码,返回生成器对象

2. 暂停与恢复执行机制(挂起帧状态)

生成器函数被调用时并不会立即执行任何函数体内部逻辑。只有后续对其调用 next() 时,代码才会运行到下一个 yield 处并发生以下行为: 1. 向外吐出数据:将 yield 后的表达式结果返回给外部调用者; 2. 挂起执行栈(Freeze):当前函数帧(Frame)的指令指针、局部变量、执行栈全部被冻结在内存中; 3. 恢复执行(Resume):当再次调用 next() 时,解释器从上次挂起的 yield 行原位唤醒,继续向下执行。

def count_down(n):
    print("--- 启动计数器 ---")
    while n > 0:
        yield n
        n -= 1
    print("--- 计数结束 ---")

g = count_down(2)
print("准备调用第一次 next:")
print(next(g))  # 启动并停在 yield 2,输出 2

print("准备调用第二次 next:")
print(next(g))  # 从 yield 2 恢复,执行 n -= 1,停在 yield 1,输出 1

# 再调用则抛出 StopIteration

四、双向通信:生成器高级方法(send / throw / close)

生成器不仅仅是单向的数据生产者,更是早前 Python 实现协程(Coroutine)的核心基础。生成器支持通过特定方法与外部调用者实现双向交互:

Caller (外部调用者)                       Generator (内部运行上下文)
     │                                            │
     │ ────────────── next() 启动 ──────────────> │ 执行到 (yield x) 处挂起,吐出 x
     │ <───────────── 接收到 x ────────────────── │
     │                                            │
     │ ──────── send("data") 传入新数据 ─────────> │ yield 表达式整体求值为 "data"
     │                                            │ 继续向下运算并停在下一个 yield

1. 使用 send(value) 向内部注入数据

def echo_accumulator():
    """双向数据累加器"""
    total = 0
    while True:
        # yield total 负责向外产出当前总和;
        # (yield total) 整体作为一个表达式,接收来自外部 send() 传入的参数值
        val = yield total
        if val is not None:
            total += val

accum = echo_accumulator()

# 首次必须通过 next() 或 send(None) 激活推进到第一个 yield 处(称为预激)
print(next(accum))       # 输出: 0

print(accum.send(10))    # 传入 10,输出: 10
print(accum.send(25))    # 传入 25,输出: 35
accum.close()            # 关闭生成器

2. 生成器核心控制方法速查

方法名 核心功能 典型场景
__next__() / next(g) 推进到下一个 yield,表达式求值为 None 标准数据流单向迭代
send(value) 唤醒生成器并将 value 赋给当前 yield 表达式 双向参数传递、状态机、经典协程任务分发
throw(typ, val, tb) 在当前挂起的 yield 处向内部抛入异常 外部主动通知任务中断、超时熔断或重试
close() 在挂起处引发 GeneratorExit,终止生成器生命周期 资源清理(关闭文件句柄、网络套接字等)

五、为什么使用 yield:内存与吞吐性能飞跃

在实际工业级场景中,yield 最具杀伤力的优势在于:将海量数据集的瞬时全量内存加载,转化为恒定低消耗的流式惰性迭代(Lazy Streaming)。

1. 经典海量数据读取:列表 vs 生成器

# ❌ 错误示范:一次性把 1000 万条日志读进内存列表 (引发内存溢出 OOM)
def read_huge_file_bad(file_path):
    result = []
    with open(file_path, "r", encoding="utf-8") as f:
        for line in f:
            result.append(line.strip())
    return result  # 若文件 10GB,瞬间占满 10GB+ 物理内存

# ✅ 工业标准:使用 yield 实现流式按需读取 (仅占几 KB 内存)
def read_huge_file_good(file_path):
    with open(file_path, "r", encoding="utf-8") as f:
        for line in f:
            yield line.strip()  # 每次仅产生一行,用完即被 GC 回收

# 消费端随用随取
for line in read_huge_file_good("/var/log/access.log"):
    if "ERROR" in line:
        process_alert(line)

2. 空间复杂度质的对比

读取 10,000,000 条记录:
- List 全量缓存方式:内存消耗 ~800 MB,首次加载延迟高
- Generator 惰性流式方式:内存消耗 ~8 KB (恒定 O(1)),首条数据 0 延迟响应

[!TIP] 生成器推导式(Generator Expression): 除了使用 def ... yield,还可以将列表推导式方括号 [] 换成圆括号 (),直接生成单行轻量生成器: gen = (x ** 2 for x in range(10000000))(仅占用极微小的迭代器开销,零数据预分配)。

admin

admin

云原生架构 · 全栈开发

感谢阅读本文!记录系统架构思考与实战复盘。专注于 Python、Django、PostgreSQL 与云原生容器化工程实践。