18-生成器不只是省内存(上)-yield的状态机模型与帧暂停

文章目录

  • [生成器不只是省内存(上):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),也肯定知道 xrangerange 在 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 个错误行号,它只在文件开头的必要行中读取,而非遍历全文件。


思考 && 总结

生成器的三个底层真相:

  1. 生成器是持久化帧对象。yield 一次暂停,帧被保存进堆。帧内所有局部变量和字节码指针(f_lasti)完整保留。
  2. next() 触发的恢复从上次暂停点开始。 CPython 恢复时跳转到保存的 f_lasti 指令处继续执行。
  3. 生成器是一次性的。 走到 StopIteration 后帧被销毁,无法回退。需要多次遍历用 list() 缓存。

结尾

各位小伙伴,上篇完毕。下篇进入 send()yield from。感谢阅读!

源码骑士 --- 源码级拆解,从底层看透技术

👀 关注:跟博主一起从源码视角深耕底层原理

❤️ 点赞:让优质内容被更多人看见

收藏:核心知识点存好,随用随查

💬 评论:分享你的经验或疑问,一起交流

🔄 一键四连:别忘了给博主一键四连!

🗡️ 寄语:生成器是 Python 对"暂停执行"这一需求的最优雅答案。

结语:生成器不是"省内存的替代方案",而是"可暂停的执行上下文"。下篇继续------send() 双向通信和 yield from 原理。一键四连!

相关推荐
我喜欢就喜欢1 小时前
C++ 连接 Ollama 本地大模型:从原生 HTTP 调用到高性能封装实践
开发语言·c++·http
长空任鸟飞_阿康1 小时前
RAG 文档摄入全链路,从原理到生产落地
vue.js·人工智能·python
程序猿零零漆1 小时前
Python核心进阶三连:闭包装饰器、深浅拷贝、网络编程从原理到实战
网络·python
yongche_shi2 小时前
ragas官方文档中文版(十六)
python·ai·智能体·ragas·使用工具
三块可乐两块冰2 小时前
rag学习5
linux·前端·python
DXM05212 小时前
第11期| 遥感图像分类模型:ResNet_DenseNet原理+实战训练
人工智能·python·深度学习·机器学习·分类·数据挖掘·ageo
SilentSamsara2 小时前
模型部署实战:FastAPI + ONNX + Docker 的推理服务化
人工智能·pytorch·python·深度学习·机器学习·fastapi
踏着七彩祥云的小丑2 小时前
Go学习第8天:接口 + 泛型 + 错误处理
开发语言·学习·golang·go
聆风吟º2 小时前
Python基础数据类型(一):数字类型
开发语言·python·float·int·bool·数字类型