文章目录
- [生成器不只是省内存(上):yield 的状态机模型------每一帧暂停与恢复的底层真相](#生成器不只是省内存(上):yield 的状态机模型——每一帧暂停与恢复的底层真相)
-
- 导入语
- [1 ~> 生成器的本质------带暂停功能的帧对象](#1 ~> 生成器的本质——带暂停功能的帧对象)
-
- [1.1 普通函数 vs 生成器函数](#1.1 普通函数 vs 生成器函数)
- [1.2 用 `gi_frame` 窥探内部状态](#1.2 用
gi_frame窥探内部状态)
- [2 ~> 生成器的状态机------四种状态](#2 ~> 生成器的状态机——四种状态)
- [3 ~> 生成器的 "一次性"------为什么不能重复迭代](#3 ~> 生成器的 "一次性"——为什么不能重复迭代)
-
- [3.1 现象](#3.1 现象)
- [3.2 原因](#3.2 原因)
- [4 ~> `yield` 暂停时局部变量怎么保存](#4 ~>
yield暂停时局部变量怎么保存) - [5 ~> 实战:一个"可恢复的日志扫描器"](#5 ~> 实战:一个"可恢复的日志扫描器")
- [思考 && 总结](#思考 && 总结)
- 结尾
生成器不只是省内存(上):yield 的状态机模型------每一帧暂停与恢复的底层真相
📖 文章简介: "生成器省内存"这个说法你肯定听过------不用一次性创建整个列表,惰性生成每个值。但本文重点不是省内存,而是深挖 yield 背后的状态机模型:生成器不是普通函数------它是一个可恢复的帧对象(Frame Object)。每次 yield 暂停时,CPython 保留下当前的局部变量状态和字节码指针,下一次 next() 从暂停点恢复执行。用 gi_frame.f_lasti 追踪字节码执行位置,解释生成器如何挂起和恢复、为什么 return 在生成器里不是结束而是抛 StopIteration、以及不能重复迭代的"一次性"特性的底层原因。

🎬 个人主页: 源码骑士
❄ 专栏传送门: 《Android开发基础》《python基础课程》
⭐️热衷从源码视角拆解技术底层原理,将复杂架构讲得通俗易懂
🎬 源码骑士的简介:
5年Android Framework系统开发经验,曾主导多项系统级性能优化专项
技术栈覆盖Android系统全链路(Binder/Handler/AMS/WMS/启动流程)及Java后端全家桶(Spring + MyBatis + Redis + Oracle)
累计产出原创技术文章100+篇,文章以源码拆解为特色,被读者评价为"看一篇胜过啃一周文档"
导入语
你肯定写过 for i in range(1000000),也肯定知道 xrange 或 range 在 Python 3 中是惰性生成器------不需要一次性创建一百万个 int 对象。这是生成器最常被提到的好处------省内存。
但生成器的真核不是"省内存"。它的本质是可恢复的栈帧 ------一个能在 yield 处暂停、状态全部保留、下次 next() 再从这个点恢复的函数。这个特性在很多高级用法里是关键------协程、异步迭代、数据管道------都基于它。
上篇从帧对象的角度讲清楚生成器到底是怎么暂停和恢复的。下篇进入 send() 和 yield from------生成器的双向通信。
1 ~> 生成器的本质------带暂停功能的帧对象
1.1 普通函数 vs 生成器函数
python
# 普通函数:调用 → 执行 → 返回 → 结束
def normal():
x = 1
x += 1
return x
# 生成器函数:调用 → 创建生成器对象 → paused
# → next() → 执行到 yield → 暂停
# → next() → 从 yield 之后恢复 → 执行到下一个 yield → 暂停
def gen():
x = 1
yield x
x += 1
yield x
普通函数的帧是一个一次性产物。函数返回后帧被销毁,局部变量不再存在。
生成器的帧是持久化的 ------yield 暂停时,帧被挂起保存,所有局部变量、字节码工具指针、栈深度全部保留。下次 next() 恢复后继续跑。
1.2 用 gi_frame 窥探内部状态
python
def simple_gen():
for i in range(3):
yield i
g = simple_gen()
# 查看生成器的内部帧对象
print(g.gi_frame) # <frame at 0x...>
print(g.gi_frame.f_lasti) # 当前执行到的字节码偏移
print(next(g)) # 0
print(g.gi_frame.f_lasti) # 字节码偏移按 yield 行变了
print(next(g)) # 1
print(next(g)) # 2
try:
next(g)
except StopIteration as e:
print(g.gi_frame) # None ← 帧已销毁
gi_frame.f_lasti 存储的是当前字节码指令的偏移量。每次 yield 暂停时,CPython 记住了这个偏移量,下一次 next() 从这个位置恢复。
2 ~> 生成器的状态机------四种状态
每个生成器内部有一个状态字段 gi_frame:
| 状态 | 含义 | 检查方式 |
|---|---|---|
| CREATED | 刚创建,还没执行过 | g.gi_frame is not None and g.gi_frame.f_lasti == -1 |
| RUNNING | 正在执行中 | 没法从外部检查 |
| SUSPENDED | 在 yield 处暂停 | g.gi_frame is not None |
| CLOSED | 已关闭(正常结束或 .close()) | g.gi_frame is None |
python
def my_gen():
yield 1
yield 2
g = my_gen()
print(g.gi_frame) # 帧存在 → CREATED
r = next(g)
print(g.gi_frame) # 帧存在 → SUSPENDED(在 yield 1 处挂起)
next(g)
next(g) # StopIteration
print(g.gi_frame) # None → CLOSED
3 ~> 生成器的 "一次性"------为什么不能重复迭代
3.1 现象
python
g = (x for x in range(3))
print(list(g)) # [0, 1, 2]
print(list(g)) # [] ← 第二次是空的!
3.2 原因
生成器走到底后帧即销毁------gi_frame 变成 None。它是一个状态机------一次性的。一旦走到了 StopIteration,状态从 SUSPENDED 进入 CLOSED,无法回退。
这和 Java 的 Iterator 一样------hasNext() 走到 false 之后就结束了,不能重新开始。你需要重新创建一个迭代器对象。
正确做法: 如果需要多次遍历,最外层用 list() 包一下缓存结果,或者直接用列表推导式而不是生成器。
4 ~> yield 暂停时局部变量怎么保存
python
def keep_track():
x = 0
while True:
received = yield x # 暂停在这里,x 的当前值被保留
if received is not None:
x = received
x += 1
g = keep_track()
print(next(g)) # 0
print(next(g)) # 1
print(next(g)) # 2
# 局部变量 x 在三次暂停之间一直保持存在------没有重新初始化
x 在每次 yield 之间持续存在。这是因为帧对象在堆上分配------帧本身不会因为函数返回被销毁。所以生成器中的局部变量就像"住在堆里的全局变量"。
5 ~> 实战:一个"可恢复的日志扫描器"
python
def log_scanner(filepath, keyword):
"""扫描日志文件,遇到 keyword 就 yield 行号"""
with open(filepath, "r") as f:
for lineno, line in enumerate(f, 1):
if keyword in line:
yield lineno # 暂停,记住当前读到哪一行
scanner = log_scanner("/var/log/app.log", "ERROR")
# 业务代码调用:
lines_with_error = []
for _ in range(10):
try:
lines_with_error.append(next(scanner)) # 逐次获取,每次只读文件的一行
except StopIteration:
break
这个 generator 的优雅之处------文件只打开一次,但业务代码能按需"拉取"结果,而不用把整个文件读进来。 如果你想获得前 10 个错误行号,它只在文件开头的必要行中读取,而非遍历全文件。
思考 && 总结
生成器的三个底层真相:
- 生成器是持久化帧对象。 每
yield一次暂停,帧被保存进堆。帧内所有局部变量和字节码指针(f_lasti)完整保留。 next()触发的恢复从上次暂停点开始。 CPython 恢复时跳转到保存的f_lasti指令处继续执行。- 生成器是一次性的。 走到
StopIteration后帧被销毁,无法回退。需要多次遍历用list()缓存。
结尾
各位小伙伴,上篇完毕。下篇进入 send() 和 yield from。感谢阅读!
源码骑士 --- 源码级拆解,从底层看透技术
👀 关注:跟博主一起从源码视角深耕底层原理
❤️ 点赞:让优质内容被更多人看见
⭐ 收藏:核心知识点存好,随用随查
💬 评论:分享你的经验或疑问,一起交流
🔄 一键四连:别忘了给博主一键四连!
🗡️ 寄语:生成器是 Python 对"暂停执行"这一需求的最优雅答案。
结语:生成器不是"省内存的替代方案",而是"可暂停的执行上下文"。下篇继续------send() 双向通信和 yield from 原理。一键四连!