码农高天
生成器表达式仅对最外层for循环立即求值,内层判断与嵌套循环在运行期才动态绑定全局变量,从而导致重赋值后的数据出现非预期交集。
AI-assisted summary of 【python】几乎没人做对的题目?聊聊生成器表达式里不为人知的秘密 — original source. Check the original for full context.
Source published:
Key ideas
- Python 生成器表达式在求值时机上存在不对称性:仅有最外层迭代对象会在定义阶段立即求值并固化,其余所有内层表达式与条件过滤均延迟至运行期解析。 (1:50)
- 根据 PEP 289 规范,如果在生成器表达式定义后、实际消费前修改全局变量,生成器内部引用的变量将动态读取最新值,从而产生反直觉的交集过滤或笛卡尔积行为。 (1:50)
- 底层字节码显示,生成器表达式被编译为独立的代码对象,外层迭代器作为初始参数传入函数,而内层条件则通过 LOAD_GLOBAL 指令在迭代运行时动态查找。 (1:50)
Detailed notes
## Background 该视频由技术博主“码农高天”于 2023 年 10 月 16 日发布在 YouTube 平台。视频简介提到,本期内容深入探讨了生成器表达式中极易被忽视、甚至作者本人第一次都想错的小细节,以此检验观众能否在不知有坑的情况下答对这道极具迷惑性的代码题。
## TL;DR 视频以一道看似荒谬的 Python 生成器代码切入,展示了当外部列表变量在生成器定义与调用之间被篡改时,输出结果既非初始列表也非修改后列表的反常识现象。高天通过拆解字节码和还原生成器内部函数,结合 PEP 289 规范阐明了根本原因:生成器表达式中仅有最外层的 for 循环会在定义期立即求值并绑定迭代器,而内部的条件判断和内层循环均遵循惰性机制,延迟到运行期才进行全局变量查找。最后,视频通过嵌套循环实战测验巩固了这一求值规则,提醒开发者警惕语法糖背后的底层语义差异。
## Atmosphere & style 视频基调轻松幽默且兼具极客探究特质,博主高天如同面对面解密悬疑谜题般抛出反直觉代码,并穿插着让观众把“懵了”打在弹幕上的调侃互动。随着底层字节码与 PEP 规范的层层推进,整体情绪从最初令人挠头的困惑迅速转化为豁然开朗的透彻感,让枯燥的语言规范讲解充满了趣味性和顿悟感。
## Bulletin - [01:50](https://www.youtube.com/watch?v=s71JhBQ3T9c&t=110s) Python 生成器表达式在求值时机上存在不对称性:仅有最外层迭代对象会在定义阶段立即求值并固化,其余所有内层表达式与条件过滤均延迟至运行期解析。 - [01:50](https://www.youtube.com/watch?v=s71JhBQ3T9c&t=110s) 根据 PEP 289 规范,如果在生成器表达式定义后、实际消费前修改全局变量,生成器内部引用的变量将动态读取最新值,从而产生反直觉的交集过滤或笛卡尔积行为。 - [01:50](https://www.youtube.com/watch?v=s71JhBQ3T9c&t=110s) 底层字节码显示,生成器表达式被编译为独立的代码对象,外层迭代器作为初始参数传入函数,而内层条件则通过 LOAD_GLOBAL 指令在迭代运行时动态查找。
## Key takeaways - [00:00](https://www.youtube.com/watch?v=s71JhBQ3T9c&t=0s) 生成器表达式遵循惰性求值原则,不会在定义时分配完整内存,而是按需产出元素。 - [00:00](https://www.youtube.com/watch?v=s71JhBQ3T9c&t=0s) 在生成器定义与实际消费之间修改全局变量时,看似恒真的条件过滤 `if n in list` 会导致反常识的输出结果。 - [00:00](https://www.youtube.com/watch?v=s71JhBQ3T9c&t=0s) 对于代码 `g = (n for n in list if n in list)`,在将 `list` 从 `[1..5]` 改为 `[0..2]` 后,实际输出为两个列表的交集 `[1, 2]`。 - [01:50](https://www.youtube.com/watch?v=s71JhBQ3T9c&t=110s) 生成器表达式在底层被编译为一个独立的代码对象,本质上等价于定义并调用一个匿名生成器函数。 - [01:50](https://www.youtube.com/watch?v=s71JhBQ3T9c&t=110s) 外部环境通过 `GET_ITER` 获取最外层迭代器,并在生成器定义时通过函数调用将其作为实参立即固化。 - [01:50](https://www.youtube.com/watch?v=s71JhBQ3T9c&t=110s) 生成器函数体内的条件判断通过 `LOAD_GLOBAL` 和 `CONTAINS` 指令运行,必须等到生成器被消费时才动态读取全局变量。 - [01:50](https://www.youtube.com/watch?v=s71JhBQ3T9c&t=110s) PEP 289 规范明确规定:生成器表达式中只有最外层的 for 表达式是立刻求值的,其余表达式均在生成器运行期间求值。 - [01:50](https://www.youtube.com/watch?v=s71JhBQ3T9c&t=110s) 等价的纯 Python 实现相当于 `def __gen(iter_arg): ...`,其中参数绑定了旧迭代器,而内部循环体则动态引用全局命名空间。 - [04:20](https://www.youtube.com/watch?v=s71JhBQ3T9c&t=260s) 在嵌套推导式 `((a, b) for a in list for b in list)` 中,最外层变量 `a` 绑定定义时的旧列表,而内层循环变量 `b` 则绑定运行期的新列表。 - [04:20](https://www.youtube.com/watch?v=s71JhBQ3T9c&t=260s) 嵌套推导式在重新赋值后的实际运行结果表现为旧列表与新列表的笛卡尔积,而非纯粹基于新列表或旧列表展开。
## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=s71JhBQ3T9c&t=0s) 生成器表达式的诡异陷阱:看似无用的条件判断
码农高天以一段关于生成器推导式(generator comprehension,通常也称为 generator expression)的趣味代码切入主题。他首先展示了三行非常简洁的基础代码:第一行定义了一个列表 `list = [1, 2, 3, 4, 5]`;第二行通过括号语法构建了一个生成器对象 `g = (n for n in list)`;第三行则通过 `list(g)` 将生成器中的元素重新聚合成列表并打印输出。与会直接开辟全新内存空间的列表推导式(list comprehension)不同,生成器表达式仅生成一个生成器对象,不会立即分配完整内存,而是遵循惰性求值原则按需产出元素。这道基础题的结果毫无悬念就是 `[1, 2, 3, 4, 5]`。
随后,高天在推导式末尾追加了一个过滤条件:`g = (n for n in list if n in list)`。表面上看,由于变量 `n` 本就是从 `list` 中迭代取出的,`if n in list` 这个判断恒为真,因此代码输出依然是 `[1, 2, 3, 4, 5]`。初看之下,这个过滤条件似乎完全是多此一举的废话。然而,高天紧接着引入了一个打破直觉的变化——在生成器定义之后、实际调用 `list(g)` 消费生成器之前,修改全局变量 `list` 的指向:
```python list = [1, 2, 3, 4, 5] g = (n for n in list if n in list) list = [0, 1, 2] print(list(g)) ```
面对修改后的代码,很多人会猜测最终输出要么是最初的 `[1, 2, 3, 4, 5]`,要么是更新后的 `[0, 1, 2]`。然而,Python 实际给出的运行结果却是出人意料的 `[1, 2]`。面对这个反常识的现象,码农高天调侃道:“如果你懵了请把懵了打在弹幕上我相信如果你之前没有认真的钻研过这件事的话你是很难想到这个结果的为什么是一二呢它完全不make sense 对不对”。
---
[01:50](https://www.youtube.com/watch?v=s71JhBQ3T9c&t=110s) 深入字节码与 PEP 289:最外层与内层的求值时机差异
为了探究为何输出会是 `[1, 2]`,高天带领观众查看了底层字节码(bytecode)。生成器表达式在底层实际上被编译成了一个独立的代码对象(code object),其本质相当于定义并调用了一个匿名的生成器函数:
```mermaid flowchart TD accTitle: 生成器表达式求值时机分流图 accDescr: 展示最外层迭代对象在定义期求值,而内部条件判断在运行期延迟求值的过程
subgraph DefinitionPhase ["生成器定义阶段(立即求值)"] A["读取外层 iterable: list = [1, 2, 3, 4, 5]"] --> B["获取迭代器: iter(list)"] B --> C["传入生成器函数作为初始参数"] end
subgraph RuntimePhase ["生成器运行阶段(延迟求值)"] C -.->|"list(g) 触发运行"| D["FOR_ITER 遍历传入的迭代器 [1, 2, 3, 4, 5]"] D --> E["LOAD_GLOBAL 读取当前全局变量 list: [0, 1, 2]"] E --> F{"CONTAINS: n in [0, 1, 2]"} F -->|"True (1, 2)"| G["Yield 产出元素"] F -->|"False (3, 4, 5)"| H["跳过"] end ```
在外部调用环境中,Python 会先执行 `LOAD_NAME` 把最初的 `list` 加载进操作数栈,调用 `GET_ITER` 取得其迭代器,随后通过 `CALL_FUNCTION` 调用生成器代码对象,并将该迭代器作为参数直接传给生成器函数。而在生成的函数体字节码内部,第二行是负责循环解包的 `FOR_ITER`,但到了第五行处理 `if n in list` 时,指令执行的却是 `LOAD_GLOBAL`,接着通过 `CONTAINS` 操作符进行包含关系比对,最后根据判断结果执行分支跳转。
这意味着在 `g = (n for n in list if n in list)` 中,看似相同的两个 `list` 实际上根本不是同一个对象: 1. **前面的 `list`**:在定义生成器表达式的瞬间就已经完成了求值并取出了迭代器,锁定的是初始的 `[1, 2, 3, 4, 5]`。 2. **后面的 `list`**:直到生成器真正被消费、进入内部函数循环执行时才动态查找全局命名空间,此时查找到的是已经被重新赋值的 `[0, 1, 2]`。
该机制下,生成器实质上是在原列表 `[1, 2, 3, 4, 5]` 中逐一取值,并逐个检查该值是否属于新列表 `[0, 1, 2]`,两者取交集正是 `[1, 2]`。
高天指出,这个设计的历史源头可以追溯到 Python 2.4 引入的 **PEP 289(Generator Expressions)**。PEP 289 明确规范了生成器表达式的等价转换逻辑与语法糖规则,并严格规定:“只有最外层的for表达式是立刻求值的而剩下的表达式都会在生成器运行的时候再求值”。
如果将该生成器表达式还原为纯 Python 的等价实现,相当于定义了如下内部生成器函数:
```python def __gen(iter_arg): for n in iter_arg: # 此处的 list 为全局查找,运行期才进行解析 if n in list: yield n
g = __gen(iter(list)) ```
外层的 `iter(list)` 在绑定时便固化了第一版列表,而内部的 `if n in list` 则在迭代调用时实时查找全局变量 `list`,从而完美解释了这一看似奇异的运行机制。
---
[04:20](https://www.youtube.com/watch?v=s71JhBQ3T9c&t=260s) 嵌套循环实战测验与求值规则总结
掌握了这一底层规则后,高天给观众留下了一道进阶思考题,考察嵌套推导式场景下的求值顺序:
```python list = [1, 2, 3] g = ((a, b) for a in list for b in list) list = [1, 2] print(list(g)) ```
在提示观众暂停思考后,高天揭晓了答案:运行结果为 `[(1, 1), (1, 2), (2, 1), (2, 2), (3, 1), (3, 2)]`。
产生这一结果的根源依然完全遵循 PEP 289 的核心定律:在嵌套推导式中,最外层的循环 `for a in list` 是立即求值的,因此在生成器构建之初,`a` 迭代的数据源就已经固定为当时的 `[1, 2, 3]`;而内层嵌套的循环 `for b in list` 属于非最外层表达式,它被延迟到生成器实际运行时才去解析变量 `list`。此时全局变量 `list` 已经被重新赋值为了 `[1, 2]`,所以 `b` 只能在 `1` 和 `2` 之间遍历。内外两层组合展开后,正好构成了 `a` 取自 1、2、3,`b` 取自 1、2 的笛卡尔积。高天最后总结这一知识点并提醒观众,看似简单的语法糖背后常常隐藏着语言规范层面的严谨细节,并在片尾呼吁大家关注并一键三连。
## Notable quotes > [01:50](https://www.youtube.com/watch?v=s71JhBQ3T9c&t=110s) 只有最外层的for表达式是立刻求值的而剩下的表达式都会在生成器运行的时候再求值 — PEP 289
## Mentioned 码农高天, Python, PEP 289, Python 2.4, YouTube
## Video description 这期视频聊聊生成器表达式里面不常见但是没啥人会的小细节,这代码我第一次都想错了。在不知道有坑的情况下,你能一次做对嘛?
[Source](https://www.youtube.com/watch?v=s71JhBQ3T9c)