码农高天
Python在编译期将包含yield的函数打上生成器标签,运行时直接依靠挂起栈帧而非类实例变量保存执行进度,使开发者无需编写迭代器类即可借助send实现双向通信。
AI-assisted summary of 【python】生成器是什么?怎么用?能干啥?一期视频解决你所有疑问! — original source. Check the original for full context.
Source published:
Key ideas
- Python 编译器在发现函数内部包含 yield 关键字时会自动打上标签,使其定义成为生成器函数;调用时不会直接执行函数体,而是返回一个生成器对象。 (0:00)
- 生成器与传统迭代器的核心区别在于状态存储机制:迭代器将状态维护在类实例的变量中,而生成器将执行进度与局部变量直接保存在运行时的栈帧(Frame)中。 (5:00)
- 生成器函数中的 return 语句在语义上等价于抛出 StopIteration 异常,其返回值无法通过普通 for 循环或 next 直接读取,必须显式捕获异常对象才能获取。 (5:00)
- 生成器支持通过 send 方法实现双向通信;yield 不仅能产出值,还能作为表达式接收外部传入的数据,且 next(g) 在底层机制上完全等价于 g.send(None)。 (7:22)
Detailed notes
## Background 该视频由 YouTube 频道“码农高天”于 2023 年 10 月 10 日发布。视频简介指出,本期主要系统性讲解 Python 中常用且重要的生成器机制,以此填补此前系列视频中提及的相关技术坑点。
## TL;DR 本期视频深入解析了 Python 生成器的核心概念与底层运作原理,指出生成器本质上是一种特殊的迭代器。作者辨析了生成器函数与生成器对象的区别,揭示了 Python 编译期通过 yield 关键字标记函数,并在运行时借助执行栈帧(Frame)自动挂起与恢复执行状态的底层字节码机制。此外,视频通过重构链表遍历对比了生成器相较传统迭代器类的简洁优势,并详细演示了如何利用 send 方法实现外部与生成器内部的双向数据通信。
## Atmosphere & style 视频整体呈现出严谨、清晰且循循善诱的技术讲座风格,极具硬核工程师的专业质感。作者以从浅入深、层层递进的逻辑展开教学,既有生动的代码对比,又直切底层字节码与执行栈帧的运作细节,让观众感受到一种豁然开朗的透彻感。整场讲解语调平稳笃定,在拆解复杂语法与通信机制时显得游刃有余、条理分明。
## Bulletin - [00:00](https://www.youtube.com/watch?v=tWOD43HQIRA&t=0s) Python 编译器在发现函数内部包含 yield 关键字时会自动打上标签,使其定义成为生成器函数;调用时不会直接执行函数体,而是返回一个生成器对象。 - [05:00](https://www.youtube.com/watch?v=tWOD43HQIRA&t=300s) 生成器与传统迭代器的核心区别在于状态存储机制:迭代器将状态维护在类实例的变量中,而生成器将执行进度与局部变量直接保存在运行时的栈帧(Frame)中。 - [05:00](https://www.youtube.com/watch?v=tWOD43HQIRA&t=300s) 生成器函数中的 return 语句在语义上等价于抛出 StopIteration 异常,其返回值无法通过普通 for 循环或 next 直接读取,必须显式捕获异常对象才能获取。 - [07:22](https://www.youtube.com/watch?v=tWOD43HQIRA&t=442s) 生成器支持通过 send 方法实现双向通信;yield 不仅能产出值,还能作为表达式接收外部传入的数据,且 next(g) 在底层机制上完全等价于 g.send(None)。
## Key takeaways - [00:00](https://www.youtube.com/watch?v=tWOD43HQIRA&t=0s) 生成器本质上是一种特殊的迭代器,同样支持 for 循环遍历以及通过 next() 函数逐步提取数据。 - [00:00](https://www.youtube.com/watch?v=tWOD43HQIRA&t=0s) 初学者极易混淆生成器函数与生成器对象:包含 yield 的 def 函数是生成器函数,调用该函数仅返回生成器对象而不执行具体代码。 - [00:00](https://www.youtube.com/watch?v=tWOD43HQIRA&t=0s) 生成器函数体内的逻辑仅在外部对其调用 next() 时才正式启动执行。 - [00:00](https://www.youtube.com/watch?v=tWOD43HQIRA&t=0s) 在底层字节码层面,遇到 YIELD_VALUE 时 Python 会将栈顶值存入返回值、记录栈深度并暂停当前栈帧退出,后续调用 next() 时直接在暂停处恢复运行。 - [00:00](https://www.youtube.com/watch?v=tWOD43HQIRA&t=0s) 在生成器函数内部,编写 return 等价于 raise StopIteration,其返回值不会在正常迭代中被打印输出。 - [05:00](https://www.youtube.com/watch?v=tWOD43HQIRA&t=300s) 若要获取生成器内部 return 所携带的具体数值,调用方必须通过 try-except 显式捕获 StopIteration 异常对象。 - [05:00](https://www.youtube.com/watch?v=tWOD43HQIRA&t=300s) 传统自定义迭代器类需要手动编写 __iter__、__next__ 并显式维护内部状态指针,模板代码相对冗长。 - [05:00](https://www.youtube.com/watch?v=tWOD43HQIRA&t=300s) 生成器自动利用 Python 栈帧记住代码执行阶段与局部变量,极大精简了诸如链表遍历等场景下的代码量。 - [07:22](https://www.youtube.com/watch?v=tWOD43HQIRA&t=442s) yield 既可以是一条独立的产出语句,也可以作为表达式在求值后将外部传入的值赋给局部变量(如 temp = yield num)。 - [07:22](https://www.youtube.com/watch?v=tWOD43HQIRA&t=442s) 生成器的底层调用机制中,next(g) 的行为与 g.send(None) 完全一致。 - [07:22](https://www.youtube.com/watch?v=tWOD43HQIRA&t=442s) 如果生成器函数中的 yield 没有赋值给变量,外部通过 send 传入的任何数据都会被静默丢弃,不会引发报错。 - [07:22](https://www.youtube.com/watch?v=tWOD43HQIRA&t=442s) send 方法为外部与生成器之间提供了交流与动态改变内部状态的机制,是 Python 协程和双向数据传递的重要底层基础。
## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=tWOD43HQIRA&t=0s) 生成器与迭代器的关系及生成器函数与对象的本质区别
在深入探讨生成器之前,码农高天强烈建议观众先看完前一期关于迭代器的内容,因为“生成器就是一种特殊的迭代器,当你理解了什么是迭代器之后,你理解生成器就比较容易了”。生成器与迭代器有很多共性:可以通过 `for i in g` 这种循环方式遍历出数据(如运行示例打印出 5、4、3、2、1),也可以使用 `next()` 函数先取出第一个值,再配合循环输出后续的值。
然而,初学者极易混淆两个概念:“生成器函数”与“生成器对象”。传统的迭代器是一个正儿八经的 `class`,实例化后得到一个迭代器对象,语义清晰不易犯错;而生成器函数的定义与普通函数几乎一模一样,都是 `def gen(...)` 格式。很多初学者看到里面写了 `return`,会误以为调用 `gen(5)` 会直接返回一个 `None` 并赋给变量 `g`。码农高天指出这是 Python 编译期的一个精妙机制:“当它发现一个函数的定义里面有 `yield` 这个关键词的时候,他就不会把这个函数当成一个普通的函数来处理。Python 会给这个函数打上一个标签,然后说这是一个生成器函数。”
```mermaid flowchart TD accTitle: Generator Function Compilation and Execution Flow accDescr: Shows how Python compiles a function with yield and executes next calls through frame pausing and resuming. subgraph DefPhase ["1. 编译与创建阶段"] A["函数定义中包含 yield 关键字"] -->|"编译期打标签"| B["生成器函数 (generator function)"] B -->|"调用 gen(5) 不执行函数体"| C["返回生成器对象 (generator object)"] end
subgraph ExecPhase ["2. 运行与暂停阶段"] C -->|"首次调用 next(g)"| D["执行 Bytecode 到 YIELD_VALUE"] D -->|"栈顶值存入 return_value"| E["暂停 Frame,记录栈深度并退出"] E -->|"再次调用 next(g)"| F["从上次暂停的 yield 处继续恢复执行"] F -->|"执行到 return 或自然结束"| G["抛出 StopIteration 异常"] end ```
- **生成器函数与对象**:`def gen(...)` 称为生成器函数;调用 `g = gen(5)` 时并不会直接执行函数本体,而是返回一个生成器对象 `g`。 - **运行机制**:只有在对生成器对象调用 `next()` 时,函数本体才真正开始执行。 - **底层字节码(Bytecode)**:当遇到 `YIELD_VALUE` 字节码时,Python 会将栈顶值取出存入返回值,将当前栈帧(frame)标记为尚未结束,保存当前的调用栈深度,然后跳转退出(`goto exiting` 返回值)。下次再次调用 `next()` 时,会在暂停的位置恢复执行。 - **`return` 的语义**:“在生成器函数里面,所以 `return` 等价于 `raise StopIteration`。” 无论 `return` 是否携带具体数值,在常规的 `next()` 调用或 `for` 循环中都不会被直接输出;唯有 `yield` 的值会被捕获并输出。
[05:00](https://www.youtube.com/watch?v=tWOD43HQIRA&t=300s) 状态保存原理对比与链表重构示例
如果想要获取生成器中 `return` 的值,不能通过常规的遍历或直接读取,而是必须显式去捕获 `StopIteration` 异常对象中的返回值。比如在生成器末尾写上 `return 100`,常规的 `for` 循环完全不会打印出任何与 100 相关的结果。
从使用者的角度来看,生成器和迭代器在使用上几乎毫无差别,但底层保存状态的方式有根本不同:“我们正常定义的迭代器也就是 iterator,它是把迭代时候的状态保存在一个对象里的;而生成器也就是 generator,它实际上是把这个状态保存在了 frame 里,就它的状态是函数运行到哪一步了,而不是通过一个变量来记录我现在的状态。”
```mermaid flowchart LR accTitle: State Storage Comparison Between Iterator and Generator accDescr: Contrasts class-based state stored in instance variables with generator state stored in the execution frame. subgraph ClassIterator ["传统迭代器 (Iterator)"] direction TB A1["自定义迭代器类"] --> A2["实例内部变量 (self.node / self.idx)"] A2 --> A3["手动更新维护状态"] end
subgraph GenIterator ["生成器 (Generator)"] direction TB B1["包含 yield 的生成器函数"] --> B2["执行栈帧 (Frame Context)"] B2 --> B3["局部变量与代码执行进度自动挂起/恢复"] end ```
码农高天引用了上期视频中的链表遍历示例进行对比说明。在传统迭代器中,需要单独写一个迭代器类并在内部维护指针状态;而使用生成器时,只需将 `__iter__` 方法写成一个生成器函数:从自身节点开始 `yield` 出当前节点,然后将指针移动到下一个节点,循环直至当前节点为 `None`。
| 特性维度 | 传统迭代器类 (Iterator Class) | 生成器 (Generator) | |:--- |:--- |:--- | | **状态存储位置** | 保存在具体类的实例对象变量中 | 保存在 Python 运行时的栈帧(Frame)中 | | **实现复杂度** | 需完整编写类定义、`__iter__` 与 `__next__` | 仅需在函数中配合循环使用 `yield` 关键字 | | **代码量** | 相对冗长,需显式维护指针与边界判定 | 代码极度精简,逻辑更加直观清晰 | | **退出控制** | 手动显式 `raise StopIteration` | 执行到普通 `return` 或代码结尾时自动抛出异常 |
- **简洁性优势**:用生成器重写链表的 `__iter__` 之后,无需创建配套的迭代器辅助类,运行结果完全一致,但在简单场景下能够大幅削减模板代码量。 - **局部变量的保留**:栈帧自动记住了局部变量以及代码执行到的位置,省去了手动管理状态变量的负担。
[07:22](https://www.youtube.com/watch?v=tWOD43HQIRA&t=442s) 高级特性 `send` 方法与双向通信机制
在基础特性之外,生成器还具备一个更为高级的用法——`send` 方法。“`send` 是什么意思呢?`send` 就是在这个生成器 `yield` 之后,把这个 `yield` 什么东西变成一个值,然后这个值还可以继续赋给这个生成器函数里的其他变量。”
通常情况下,`yield num` 仅仅是一条语句,但它也可以作为一个表达式被赋值,例如 `temp = yield num`。执行时,生成器会先将当前的值 `yield` 出去(无论外部是通过 `next()` 还是 `send()` 触发),随后代码在此处暂停,等待外部通过 `send()` 传递进来的值;这个值就像是让 `yield` 表达式完成了一次求值求出来的结果。
```mermaid flowchart TD accTitle: Bidirectional Communication via Generator send Method accDescr: Illustrates how values are yielded out and new values are sent into a generator to modify internal variables. A["外部首次调用 next(g)"] --> B["函数执行到 temp = yield num,输出值 5 并暂停"] B --> C["外部调用 g.send(10)"] C --> D["yield 表达式被求值为 10,并赋值给 temp"] D --> E["检测到 temp 不是 None,将 num 重置为 10"] E --> F["执行 num -= 1 后循环回到 yield,输出值 9 并再次暂停"] F --> G["外部通过 for 循环执行,等价于连续调用 g.send(None)"] G --> H["temp 被赋予 None,内部数值正常递减输出 8 到 1"] ```
在修改后的程序中,首次调用 `first = next(g)` 先取出了初始值 5;接着调用 `g.send(10)`,传入的 10 作为 `yield` 表达式的返回值赋给了 `temp`。生成器内部检测到 `temp` 不是 `None`,便把主循环变量 `num` 重新赋值为 10,随后执行减 1 操作,因此紧接着产出的值为 9。
- **`next()` 与 `send(None)` 的等价性**:在底层机制上,“对于生成器来说,`next(g)` 和 `g.send(None)` 是一模一样的。”在随后的 `for i in g` 循环中,由于循环底层不断调用的实质等同于 `g.send(None)`,变量 `temp` 每次都会被赋为 `None`,不会再触发重置逻辑,数值便从 8 一路按步长递减输出到 1。 - **无赋值时的静默丢弃**:如果生成器函数中的 `yield num` 只是作为单条独立语句存在,没有赋值给任何变量,那么无论外部 `send` 什么数值,该值都会被静默丢弃,不会影响状态,也不会使生成器崩溃。此时 `send` 任意值和使用 `next()` 表现完全相同。 - **核心定位与价值**:“`send` 函数只是给你一个机制,让你可以去和你的 generator 做沟通和交流,可以改变他的一些状态。”它为 Python 协程和生成器的双向数据传递提供了底层的语言支撑。
## Notable quotes > [05:00](https://www.youtube.com/watch?v=tWOD43HQIRA&t=300s) 我们正常定义的迭代器也就是 iterator,它是把迭代时候的状态保存在一个对象里的;而生成器也就是 generator,它实际上是把这个状态保存在了 frame 里 — 码农高天
> [00:00](https://www.youtube.com/watch?v=tWOD43HQIRA&t=0s) 生成器就是一种特殊的迭代器,当你理解了什么是迭代器之后,你理解生成器就比较容易了 — 码农高天
> [07:22](https://www.youtube.com/watch?v=tWOD43HQIRA&t=442s) send 函数只是给你一个机制,让你可以去和你的 generator 做沟通和交流,可以改变他的一些状态。 — 码农高天
## Mentioned Python, 码农高天
## Video description 这期视频讲一下生成器,在python中还是比较常用的。也是填上了之前挖了好多次的坑。
[Source](https://www.youtube.com/watch?v=tWOD43HQIRA)