当前位置: 首页 > news >正文

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

文章目录

  • 生成器不只是省内存(上):yield 的状态机模型——每一帧暂停与恢复的底层真相
    • 导入语
    • 1 ~> 生成器的本质——带暂停功能的帧对象
      • 1.1 普通函数 vs 生成器函数
      • 1.2 用 `gi_frame` 窥探内部状态
    • 2 ~> 生成器的状态机——四种状态
    • 3 ~> 生成器的 "一次性"——为什么不能重复迭代
      • 3.1 现象
      • 3.2 原因
    • 4 ~> `yield` 暂停时局部变量怎么保存
    • 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 生成器函数

# 普通函数:调用 → 执行 → 返回 → 结束defnormal():x=1x+=1returnx# 生成器函数:调用 → 创建生成器对象 → paused# → next() → 执行到 yield → 暂停# → next() → 从 yield 之后恢复 → 执行到下一个 yield → 暂停defgen():x=1yieldx x+=1yieldx

普通函数的帧是一个一次性产物。函数返回后帧被销毁,局部变量不再存在。

生成器的帧是持久化的——yield暂停时,帧被挂起保存,所有局部变量、字节码工具指针、栈深度全部保留。下次next()恢复后继续跑。

1.2 用gi_frame窥探内部状态

defsimple_gen():foriinrange(3):yieldi g=simple_gen()# 查看生成器的内部帧对象print(g.gi_frame)# <frame at 0x...>print(g.gi_frame.f_lasti)# 当前执行到的字节码偏移print(next(g))# 0print(g.gi_frame.f_lasti)# 字节码偏移按 yield 行变了print(next(g))# 1print(next(g))# 2try:next(g)exceptStopIterationase: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
defmy_gen():yield1yield2g=my_gen()print(g.gi_frame)# 帧存在 → CREATEDr=next(g)print(g.gi_frame)# 帧存在 → SUSPENDED(在 yield 1 处挂起)next(g)next(g)# StopIterationprint(g.gi_frame)# None → CLOSED

3 ~> 生成器的 “一次性”——为什么不能重复迭代

3.1 现象

g=(xforxinrange(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暂停时局部变量怎么保存

defkeep_track():x=0whileTrue:received=yieldx# 暂停在这里,x 的当前值被保留ifreceivedisnotNone:x=received x+=1g=keep_track()print(next(g))# 0print(next(g))# 1print(next(g))# 2# 局部变量 x 在三次暂停之间一直保持存在——没有重新初始化

x在每次 yield 之间持续存在。这是因为帧对象在堆上分配——帧本身不会因为函数返回被销毁。所以生成器中的局部变量就像"住在堆里的全局变量"。


5 ~> 实战:一个"可恢复的日志扫描器"

deflog_scanner(filepath,keyword):"""扫描日志文件,遇到 keyword 就 yield 行号"""withopen(filepath,"r")asf:forlineno,lineinenumerate(f,1):ifkeywordinline:yieldlineno# 暂停,记住当前读到哪一行scanner=log_scanner("/var/log/app.log","ERROR")# 业务代码调用:lines_with_error=[]for_inrange(10):try:lines_with_error.append(next(scanner))# 逐次获取,每次只读文件的一行exceptStopIteration:break

这个 generator 的优雅之处——文件只打开一次,但业务代码能按需"拉取"结果,而不用把整个文件读进来。如果你想获得前 10 个错误行号,它只在文件开头的必要行中读取,而非遍历全文件。


思考 && 总结

生成器的三个底层真相:

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

结尾

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

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

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

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

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

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

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

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

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

http://www.jsqmd.com/news/1013899/

相关文章:

  • 合肥市庐江县 家电维修清洗|维小达|空调、冰箱、洗衣机、热水器、油烟机一站式维保清洗服务 - 维小达科技
  • 广州擅长合同诈骗刑事辩护律师排名参考:2026 年经济犯罪辩护实务观察 - 互联网科技品牌测评
  • 跨平台BongoCat交互式桌宠:从事件捕获到视觉反馈的实时响应机制
  • Claudesidian:打造AI驱动的第二大脑,让知识管理从未如此简单高效
  • Java Web WEB旅游推荐系统系统源码-SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0【含文档】
  • 面试官最爱挖的“数学陷阱”:有序转数组(Sort Transformed Array)为什么很多人第一眼就做错了?
  • Yuzu模拟器企业级部署方案:3种架构设计与性能优化50%技术指南
  • 2026年6月最新版晋城正规房屋漏水防水补漏维修口碑名单:创维修缮机构等5家深度测评 - 一修哥咨询
  • MPC8309 USB OTG驱动开发:从寄存器解析到实战避坑指南
  • 2026 Lazada流量转化导师客观测评榜单|商家选型避坑指南 - 品牌2026推荐
  • CPython性能优化:如何深度理解Python解释器运行机制
  • 告别命令行烦恼:将SillyTavern打造成真正的桌面应用,享受一键启动的AI聊天体验
  • Java 开发者怎么用 Spring AI 接 DeepSeek?一个最小 Demo 跑通思路
  • 高压型侧装式磁翻板液位计UXJC-1260-1-A-2
  • 海外仓建站方案:打造国际物流服务营销平台 - 外贸营销驿站
  • 2026温州GEO优化公司权威评测报告:企业AI搜索选型避坑指南 - 品牌报告
  • 2026电商流量转化实战专家机构客观测评榜单:企业全域转化选型指南 - 品牌2026推荐
  • FDC故障检测规则设计:从人工经验到AI自动学习
  • 2026年6月最新版淮安正规房屋漏水防水补漏维修口碑名单:创维修缮机构等5家深度测评 - 一修哥咨询
  • MPC8306定时器模块详解:RTC、PIT与GTM的设计原理与工程实践
  • 3步搞定洛雪音乐音源配置:免费获取全网无损音乐的终极方案
  • LeetCode 高频题解:滑动窗口与双指针的通用解题框架
  • 半导体工艺参数优化:用贝叶斯优化替代试错法
  • 2026年浪琴全国售后网络全新升级(最新服务热线与网点地址汇总) - 资讯速览
  • MTK8088单板机制作(二)激活测试
  • 2026青岛奢侈品回收口碑老店 正规商家盘点 - 资讯速览
  • Java+AI全栈工程师新一代技术人才的进化之路
  • 解锁Dify工作流魔法:零代码打造小红书爆款卡片
  • 2026年6月最新版喀什正规房屋漏水防水补漏维修口碑名单:创维修缮机构等5家深度测评 - 一修哥咨询
  • 2026年6月最新版晋中正规房屋漏水防水补漏维修口碑名单:创维修缮机构等5家深度测评 - 一修哥咨询