文章目录
-
- 一、从一个内存溢出场景开始
- 二、生成器是什么:从迭代器协议出发
-
- [2.1 如何识别生成器函数](#2.1 如何识别生成器函数)
- [三、`yield` 的暂停与恢复机制](#三、
yield的暂停与恢复机制) -
- [3.1 帧对象的持久化](#3.1 帧对象的持久化)
- 四、生成器表达式:列表推导式的惰性版本
-
- [4.1 生成器表达式作为函数参数](#4.1 生成器表达式作为函数参数)
- [4.2 嵌套生成器表达式](#4.2 嵌套生成器表达式)
- 五、生成器的工程价值:惰性求值的三个维度
-
- [5.1 内存效率:处理超过 RAM 容量的数据](#5.1 内存效率:处理超过 RAM 容量的数据)
- [5.2 计算效率:只计算真正需要的值](#5.2 计算效率:只计算真正需要的值)
- [5.3 表达力:把复杂的状态机写成顺序代码](#5.3 表达力:把复杂的状态机写成顺序代码)
- [六、实战案例:ETL 数据管道](#六、实战案例:ETL 数据管道)
- 七、生成器的状态与生命周期
-
- [7.1 `close()` 与资源释放](#7.1
close()与资源释放)
- [7.1 `close()` 与资源释放](#7.1
- [八、`return` 在生成器中的特殊含义](#八、
return在生成器中的特殊含义) - 九、性能对比:何时选择生成器,何时选择列表
- 十、常见错误与陷阱
-
- [错误一:在生成器函数里混用 return 和 yield(Python 2 遗留习惯)](#错误一:在生成器函数里混用 return 和 yield(Python 2 遗留习惯))
- [错误二:误以为 yield 会立即执行](#错误二:误以为 yield 会立即执行)
- [错误三:在生成器外层捕获 StopIteration(PEP 479)](#错误三:在生成器外层捕获 StopIteration(PEP 479))
- 错误四:期望生成器表达式在定义时绑定外部变量
- 十一、与前序文章的联系
- 小结
一、从一个内存溢出场景开始
有一项任务:处理日志文件,统计其中所有包含 ERROR 的行。
方案 A(直觉写法):
python
def get_error_lines(filepath):
with open(filepath, "r", encoding="utf-8") as f:
lines = f.readlines() # 把整个文件读入内存
return [line for line in lines if "ERROR" in line]
errors = get_error_lines("system.log")
for error in errors:
print(error.rstrip())
当 system.log 有 10 万行、每行 200 字节时,readlines() 会一次性占用约 20MB 内存。如果文件有 1GB,这个函数会让进程 OOM(内存耗尽)崩溃。
方案 B(生成器写法):
python
def get_error_lines(filepath):
with open(filepath, "r", encoding="utf-8") as f:
for line in f:
if "ERROR" in line:
yield line # 每次只产出一行
for error in get_error_lines("system.log"):
print(error.rstrip())
两段代码的外部调用方式完全相同,但内存占用从"整个文件"变成了"当前处理的一行"。无论文件有多大,内存使用始终稳定。
这就是生成器(Generator)最直观的工程价值------以时间换空间,用惰性求值代替急切求值。
二、生成器是什么:从迭代器协议出发
上一篇(#05)建立了迭代器协议的完整模型:任何实现了 __iter__ 和 __next__ 的对象都是迭代器。但手动编写迭代器类需要大量样板代码:
python
class EvenNumbers:
"""手动实现的偶数迭代器(样板代码较多)"""
def __init__(self, limit):
self.current = 0
self.limit = limit
def __iter__(self):
return self
def __next__(self):
if self.current >= self.limit:
raise StopIteration
value = self.current
self.current += 2
return value
生成器函数提供了一种等价但极其简洁的写法:
python
def even_numbers(limit):
current = 0
while current < limit:
yield current
current += 2
两者的行为完全等价,但生成器版本把状态管理(self.current)、协议实现(__iter__、__next__)、结束信号(StopIteration)全部隐藏到了语言机制里。
2.1 如何识别生成器函数
判断标准 :函数体内包含至少一个 yield 语句(或 yield from 语句),该函数就是生成器函数。
调用生成器函数不会执行函数体 ,而是立即返回一个生成器对象:
python
def simple_gen():
print("执行到第一个 yield 前")
yield 1
print("执行到第二个 yield 前")
yield 2
print("函数体执行完毕")
# 调用生成器函数:什么都没发生,没有任何输出
gen = simple_gen()
print(type(gen)) # <class 'generator'>
print(gen) # <generator object simple_gen at 0x...>
# 第一次 next():执行到第一个 yield,暂停
value = next(gen)
# 打印:"执行到第一个 yield 前"
print(value) # 1
# 第二次 next():从上次暂停的位置继续
value = next(gen)
# 打印:"执行到第二个 yield 前"
print(value) # 2
# 第三次 next():执行剩余代码,然后抛出 StopIteration
try:
next(gen)
# 打印:"函数体执行完毕"
except StopIteration:
print("生成器耗尽")
三、yield 的暂停与恢复机制
yield 最独特的地方在于:它不只是"返回一个值",而是让函数的执行状态完整冻结,等待下次被唤醒时从同一位置继续。
这与普通函数的 return 有根本区别:
| 特性 | return |
yield |
|---|---|---|
| 执行后 | 函数帧销毁,局部变量消失 | 函数帧被挂起,局部变量保留 |
| 再次调用 | 从头开始执行 | 从上次 yield 之后继续 |
| 调用次数 | 每次调用是独立的 | 多次 next() 是同一个执行流 |
| 内存模型 | 栈帧弹出 | 栈帧保持活跃(heap 中的帧对象) |
帧对象 (f_locals, f_lasti) 生成器对象 调用方 (for 循环 / next()) 帧对象 (f_locals, f_lasti) 生成器对象 调用方 (for 循环 / next()) next(gen) 第1次 恢复执行(从头开始) yield 1(挂起,保存 f_lasti) 返回值 1 next(gen) 第2次 恢复执行(从 f_lasti 继续) yield 2(挂起,保存 f_lasti) 返回值 2 next(gen) 第3次 恢复执行(从 f_lasti 继续) return / 函数体结束 抛出 StopIteration
3.1 帧对象的持久化
在 CPython 中,生成器对象内部持有一个帧对象 (frame object),帧对象保存了函数执行的完整上下文:
python
def stateful_gen():
x = 10
y = 20
yield x + y # 帧在这里冻结,x=10, y=20 都被保留
z = 30
yield x + y + z # 恢复执行,x, y 依然可用
gen = stateful_gen()
print(gen.gi_frame) # 帧对象
print(gen.gi_frame.f_locals) # 当前局部变量(执行前为空)
next(gen) # 执行到第一个 yield
print(gen.gi_frame.f_locals) # {'x': 10, 'y': 20}
next(gen) # 执行到第二个 yield
print(gen.gi_frame.f_locals) # {'x': 10, 'y': 20, 'z': 30}
四、生成器表达式:列表推导式的惰性版本
Python 提供了生成器表达式语法,形式上与列表推导式几乎相同,只是将 [] 换成 ():
python
# 列表推导式:立即计算,全部存入内存
squares_list = [x**2 for x in range(1_000_000)] # 约 8MB 内存
# 生成器表达式:惰性计算,按需产出
squares_gen = (x**2 for x in range(1_000_000)) # 几乎 0 内存
生成器表达式在很多场景下可以直接替代列表推导式:
python
import sys
data = range(1, 10001)
# 内存占用对比
list_comp = [x**2 for x in data]
gen_expr = (x**2 for x in data)
print(f"列表推导式占用:{sys.getsizeof(list_comp):,} bytes")
# 列表推导式占用:85,176 bytes
print(f"生成器表达式占用:{sys.getsizeof(gen_expr):,} bytes")
# 生成器表达式占用:112 bytes(始终固定大小)
4.1 生成器表达式作为函数参数
当函数只接受一个可迭代对象时,生成器表达式可以省略外层括号:
python
# 标准写法(两层括号)
total = sum((x**2 for x in range(1, 101)))
# 可以省略内层括号(只在单参数调用时成立)
total = sum(x**2 for x in range(1, 101))
# 多个参数时不能省略
combined = list(zip(
(x for x in range(5)),
(y**2 for y in range(5))
))
4.2 嵌套生成器表达式
python
# 展平二维列表(惰性版本)
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flattened = (item for row in matrix for item in row)
print(list(flattened)) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
五、生成器的工程价值:惰性求值的三个维度
5.1 内存效率:处理超过 RAM 容量的数据
python
def read_large_csv(filepath: str):
"""逐行解析 CSV,不把整个文件载入内存"""
import csv
with open(filepath, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
yield row
def filter_active_users(rows):
"""过滤活跃用户"""
for row in rows:
if row.get("status") == "active":
yield row
def enrich_with_score(rows):
"""为每行添加评分字段"""
for row in rows:
score = int(row.get("purchase_count", 0)) * 10
yield {**row, "score": score}
# 三个生成器串联,任何时刻内存中只有"当前行"
pipeline = enrich_with_score(
filter_active_users(
read_large_csv("users_10gb.csv")
)
)
for user in pipeline:
save_to_database(user)
这个模式称为生成器流水线(Generator Pipeline)------每个生成器是流水线上的一道工序,数据以单条记录为单位流过整个管道,与磁盘文件大小无关,内存始终稳定。
5.2 计算效率:只计算真正需要的值
python
def find_first_prime_over(n: int) -> int:
"""找到第一个大于 n 的质数"""
def is_prime(num):
if num < 2:
return False
for i in range(2, int(num**0.5) + 1):
if num % i == 0:
return False
return True
def primes_from(start):
"""无限质数序列(惰性的)"""
candidate = start
while True:
if is_prime(candidate):
yield candidate
candidate += 1
# next() 只计算到找到第一个质数为止,后续的永不计算
return next(primes_from(n + 1))
print(find_first_prime_over(100)) # 101
print(find_first_prime_over(1000)) # 1009
如果用列表来实现"无限质数序列",程序会立即死循环。生成器天然支持无限序列,因为它只在被请求时计算下一个值。
5.3 表达力:把复杂的状态机写成顺序代码
经典问题:解析括号嵌套的表达式树。用普通函数需要维护一个显式的状态变量;用生成器可以把解析过程写成线性的"遇到什么做什么":
python
def tokenize(expr: str):
"""将表达式字符串分解为 token 序列"""
token = []
for char in expr:
if char in "()":
if token:
yield "".join(token).strip()
token = []
if char.strip():
yield char
elif char == ",":
if token:
yield "".join(token).strip()
token = []
else:
token.append(char)
if token:
yield "".join(token).strip()
tokens = list(tokenize("add(mul(2, 3), sub(10, 4))"))
print(tokens)
# ['add', '(', 'mul', '(', '2', '3', ')', 'sub', '(', '10', '4', ')', ')']
生成器让解析器状态以局部变量的形式自然存活,无需在对象属性里显式管理。
六、实战案例:ETL 数据管道
下面是一个更完整的工程场景:从多个 JSON 文件中提取、转换、加载数据。
python
import json
import pathlib
from typing import Iterator
from datetime import datetime
# === Extract 阶段:从多个文件读取原始事件 ===
def extract_events(data_dir: str) -> Iterator[dict]:
"""
扫描目录下所有 .json 文件,逐条产出事件记录。
单文件可能有数百万行,不预先全部读入内存。
"""
base = pathlib.Path(data_dir)
for json_file in base.rglob("*.json"):
with open(json_file, encoding="utf-8") as f:
for line in f:
line = line.strip()
if line:
try:
yield json.loads(line)
except json.JSONDecodeError:
pass # 跳过损坏的行
# === Transform 阶段:数据清洗与标准化 ===
def normalize_timestamp(events: Iterator[dict]) -> Iterator[dict]:
"""将不同格式的时间戳统一转换为 ISO 8601"""
for event in events:
ts = event.get("timestamp") or event.get("ts") or event.get("time")
if ts:
if isinstance(ts, (int, float)):
event["timestamp"] = datetime.fromtimestamp(ts).isoformat()
elif isinstance(ts, str):
event["timestamp"] = ts # 假设已是标准格式
yield event
def filter_by_type(events: Iterator[dict], event_type: str) -> Iterator[dict]:
"""只保留指定类型的事件"""
for event in events:
if event.get("type") == event_type:
yield event
def add_processing_meta(events: Iterator[dict]) -> Iterator[dict]:
"""为每条事件添加处理元数据"""
processed_at = datetime.now().isoformat()
for i, event in enumerate(events):
yield {
**event,
"_processed_at": processed_at,
"_sequence": i,
}
# === Load 阶段:按批次写入目标系统 ===
def batch_load(events: Iterator[dict], batch_size: int = 500):
"""
将事件收集成批次,批量写入(减少 I/O 次数)。
利用生成器实现"按需收集",不预先构建所有批次。
"""
batch = []
for event in events:
batch.append(event)
if len(batch) >= batch_size:
yield batch
batch = []
if batch: # 不要忘记最后一个不完整的批次
yield batch
# === 组装完整管道 ===
def run_pipeline(data_dir: str, target_event_type: str):
raw = extract_events(data_dir)
with_ts = normalize_timestamp(raw)
filtered = filter_by_type(with_ts, target_event_type)
enriched = add_processing_meta(filtered)
batches = batch_load(enriched, batch_size=500)
total = 0
for batch in batches:
# write_to_warehouse(batch) # 实际写入目标数据库
total += len(batch)
print(f"已写入 {total} 条记录", end="\r")
print(f"\n管道完成,共处理 {total} 条 '{target_event_type}' 事件")
整个管道从数据目录到最终写入,中间每一步都是生成器------任何时刻内存里只存在"当前正在处理的那条记录"加上"当前积累的批次"。
七、生成器的状态与生命周期
每个生成器对象都有四个状态,可通过 inspect.getgeneratorstate() 查询:
调用生成器函数
next() 或 send()
遇到 yield
next() 或 send()
return 或函数体结束
.close() 或被垃圾回收
GEN_CREATED
GEN_RUNNING
GEN_SUSPENDED
GEN_CLOSED
python
import inspect
def lifecycle_demo():
yield 1
yield 2
gen = lifecycle_demo()
print(inspect.getgeneratorstate(gen)) # GEN_CREATED
next(gen)
print(inspect.getgeneratorstate(gen)) # GEN_SUSPENDED
next(gen)
print(inspect.getgeneratorstate(gen)) # GEN_SUSPENDED
try:
next(gen)
except StopIteration:
pass
print(inspect.getgeneratorstate(gen)) # GEN_CLOSED
7.1 close() 与资源释放
当生成器持有文件句柄、数据库连接等资源时,需要确保生成器被正确关闭:
python
def resource_holding_gen(filepath):
with open(filepath, "r") as f: # 文件在此打开
for line in f:
yield line.rstrip()
# 函数体结束或 .close() 被调用时,with 块的 __exit__ 确保文件关闭
gen = resource_holding_gen("data.txt")
next(gen)
next(gen)
gen.close() # 触发 GeneratorExit,文件正确关闭
# 即使不显式调用 close(),垃圾回收时也会自动调用
# 更好的做法:用完即销毁,依赖 with 语句
当 .close() 被调用时,Python 在生成器挂起的位置抛出 GeneratorExit 异常。with 语句的 __exit__ 会捕获这个异常并完成清理。这是生成器与上下文管理器协作的基础。
八、return 在生成器中的特殊含义
生成器函数里的 return 语句不返回值给调用方,而是终止生成器并将返回值附在 StopIteration 异常的 value 属性上:
python
def gen_with_return():
yield 1
yield 2
return "finished" # 这里的 "finished" 不会被 for 循环看到
gen = gen_with_return()
print(next(gen)) # 1
print(next(gen)) # 2
try:
next(gen)
except StopIteration as e:
print(e.value) # finished
这个机制的用途在 yield from 中会发挥关键作用------子生成器的 return 值会成为 yield from 表达式的值,这是下一篇(#07)的重点内容。
九、性能对比:何时选择生成器,何时选择列表
生成器不总是比列表好,两者有各自的适用场景:
python
import time
import sys
N = 1_000_000
# 场景 1:只需要遍历一次,不需要随机访问
def bench_single_pass():
# 列表:先全部生成,再遍历
t0 = time.perf_counter()
total = sum([x**2 for x in range(N)])
t1 = time.perf_counter()
list_time = t1 - t0
# 生成器:惰性生成,边产出边求和
t0 = time.perf_counter()
total = sum(x**2 for x in range(N))
t1 = time.perf_counter()
gen_time = t1 - t0
print(f"列表推导式:{list_time:.3f}s,内存约 {N*8//1024//1024}MB")
print(f"生成器表达式:{gen_time:.3f}s,内存约 112B")
bench_single_pass()
# 列表推导式:0.089s,内存约 7MB
# 生成器表达式:0.071s,内存约 112B
选择指引:
| 场景 | 推荐 | 原因 |
|---|---|---|
需要随机访问(lst[i]) |
列表 | 生成器不支持索引访问 |
| 需要多次遍历 | 列表 | 生成器一次性消耗 |
需要 len() |
列表 | 生成器无法预先知道长度 |
| 数据量超过内存 | 生成器 | 核心使用场景 |
| 无限序列 | 生成器 | 列表无法表示无限数据 |
| 只遍历一次的流式数据 | 生成器 | 内存高效 |
| 流水线中间结果 | 生成器 | 避免中间数据物化 |
十、常见错误与陷阱
错误一:在生成器函数里混用 return 和 yield(Python 2 遗留习惯)
python
# Python 3 中合法:return 用于提前终止生成器
def gen_until_sentinel(data, sentinel):
for item in data:
if item == sentinel:
return # 提前终止,等价于不再 yield
yield item
list(gen_until_sentinel([1, 2, "STOP", 3, 4], "STOP"))
# [1, 2]
错误二:误以为 yield 会立即执行
python
def deferred():
print("这行代码在 next() 被调用前不会执行")
yield 42
gen = deferred() # 什么都不发生,没有打印
val = next(gen) # 这里才打印,然后暂停在 yield
错误三:在生成器外层捕获 StopIteration(PEP 479)
python
# Python 3.7+ 起,生成器内部逃逸的 StopIteration 会变成 RuntimeError
def buggy_gen():
it = iter([1, 2])
while True:
yield next(it) # 当 it 耗尽,StopIteration 从 next() 逃出
# Python 3.7+ 会将其转换为 RuntimeError!
# 正确写法
def correct_gen():
it = iter([1, 2])
while True:
try:
yield next(it)
except StopIteration:
return
错误四:期望生成器表达式在定义时绑定外部变量
python
# 陷阱:生成器表达式中对外部变量的引用是惰性的
multiplier = 2
gen = (x * multiplier for x in range(5))
multiplier = 10 # 修改了外部变量
print(list(gen)) # [0, 10, 20, 30, 40] ← 使用了修改后的值!
# 如果需要在定义时捕获值,用默认参数技巧
gen = (x * m for x in range(5) for m in [multiplier])
# 或者直接用列表推导式
result = [x * 2 for x in range(5)] # 值在定义时已固定
十一、与前序文章的联系
生成器是迭代器协议(#05)的最优雅实现,它把手动编写 __iter__、__next__、StopIteration 的工作,隐藏到了 yield 关键字背后。
生成器对象同时也是一个闭包的特殊形式------它捕获了函数帧里的所有局部变量,就像 Python 进阶 #02 中讨论的闭包捕获自由变量。两者的区别在于:普通闭包捕获的是对外部作用域变量的引用 ,而生成器保存的是整个执行帧(包括程序计数器位置),支持从中断点继续执行。
这个"可暂停的执行帧"概念,正是 Python 异步编程(asyncio、协程)的底层基础------协程本质上就是一个可以被调度器暂停和恢复的生成器。
小结
- 生成器函数含有
yield,调用后返回生成器对象而非立即执行 yield冻结函数执行帧,next()恢复执行,StopIteration信号结束- 生成器是惰性的:不需要的值永远不会计算,支持无限序列和超大数据集处理
- 生成器表达式是列表推导式的惰性版本,仅将
[]替换为() - 生成器流水线将多个惰性变换串联,在不增加内存的前提下完成复杂的数据处理
return在生成器中表示终止,其值附在StopIteration.value上- Python 3.7+ 起,生成器内部逃逸的
StopIteration会变成RuntimeError(PEP 479)
文章内容若有帮助,点个赞再走------每一个赞都是继续深挖 Python 底层机制的动力。收藏不点赞等于白嫖(手动狗头)。本专栏持续更新,欢迎关注,和更多 Python 开发者一起把基础打牢。