Python 3.13修复推导式内联与闭包判定冲突导致的段错误

这五行Python程序的结果,谁都不可能猜得到 — 码农高天

码农高天

PEP 667依赖静态编译信息解析locals,遇到PEP 709动态剥离闭包变量的内联推导式时会强行解包非cell对象,最终通过在C语言层补充类型检查消除崩溃。

AI-assisted summary of 这五行Python程序的结果,谁都不可能猜得到 — original source. Check the original for full context.

Source published:

Key ideas

  • 一段仅有五行的 Python 代码在跨版本执行时表现大相径庭:Python 3.11 及更早版本字典输出包含特殊变量 '.0',3.12 仅保留普通局部变量,而 3.13 则会直接因段错误(Segmentation fault)崩溃。 (0:00)
  • Python 3.12 的 PEP 709 引入了推导式内联(Inline Comprehensions),取消了原先独立创建 code object 的函数调用开销,改用 LOAD_FAST_AND_CLEAR 在当前栈帧内实现变量隔离。 (0:00)
  • Python 3.13 崩溃的主因是 PEP 667 重构 locals() 时,解释器依靠静态编译期信息判定闭包变量,却在运行时遇到了内联推导式动态写入的非 cell 对象,导致强行解包引发段错误。 (5:00)
  • 官方最终通过在 C 层增加动态类型检查修复了该崩溃,尽管在手动传入 cell_type 的极端边界情况下仍存在理论瑕疵,但在工程权衡中成功消除了致命的段错误缺陷。 (5:00)

Detailed notes

## Background 本内容由 YouTube 频道“码农高天”于 2025 年 2 月 22 日发布。视频简介指出,短短五行的 Python 程序背后蕴含着诸多底层学问,探讨了从 PEP 709 到 PEP 667 等多个特性的改动如何机缘巧合地凑在一起,最终引发了一个极其罕见的特殊运行案例。

## TL;DR 视频深入剖析了一段仅有五行却在不同 Python 版本中表现截然不同的代码:在 Python 3.11 及更早版本中会输出包含迭代器临时变量的字典,在 3.12 中得益于 PEP 709 的推导式内联优化移除了临时变量,而在 3.13 中则因 PEP 667 对 locals 机制的重构直接导致了段错误崩溃。崩溃的根本原因在于静态编译期对闭包变量的判定与运行时内联推导式动态复用变量槽位之间的冲突,导致解释器尝试对普通整数执行解包操作。码农高天详细解释了底层的字节码运行逻辑,并分享了自己在 CPython 源码中提交动态校验补丁的修复过程与工程权衡。

## Atmosphere & style 视频开篇带着一种技术黑客式的挑战趣味,主讲人自信断言无人能猜中输出,迅速勾起观众的好奇心与解题欲。随着 CPython 字节码与栈帧机制的层层剥离,语调转向严谨透彻的硬核源码教学,穿插着作者自嘲“大傻子做改动”的幽默风趣。整体观感既像是在看一场抽丝剥茧的底层 Debug 侦探秀,又在结尾升华出工业级软件工程中“实用优于完美”的深刻感悟。

## Bulletin - [00:00](https://www.youtube.com/watch?v=ltCNkcl9HGI&t=0s) 一段仅有五行的 Python 代码在跨版本执行时表现大相径庭:Python 3.11 及更早版本字典输出包含特殊变量 '.0',3.12 仅保留普通局部变量,而 3.13 则会直接因段错误(Segmentation fault)崩溃。 - [00:00](https://www.youtube.com/watch?v=ltCNkcl9HGI&t=0s) Python 3.12 的 PEP 709 引入了推导式内联(Inline Comprehensions),取消了原先独立创建 code object 的函数调用开销,改用 LOAD_FAST_AND_CLEAR 在当前栈帧内实现变量隔离。 - [05:00](https://www.youtube.com/watch?v=ltCNkcl9HGI&t=300s) Python 3.13 崩溃的主因是 PEP 667 重构 locals() 时,解释器依靠静态编译期信息判定闭包变量,却在运行时遇到了内联推导式动态写入的非 cell 对象,导致强行解包引发段错误。 - [05:00](https://www.youtube.com/watch?v=ltCNkcl9HGI&t=300s) 官方最终通过在 C 层增加动态类型检查修复了该崩溃,尽管在手动传入 cell_type 的极端边界情况下仍存在理论瑕疵,但在工程权衡中成功消除了致命的段错误缺陷。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=ltCNkcl9HGI&t=0s) 相同五行 Python 代码在 3.11(含 '.0' 键)、3.12(无 '.0')和 3.13(直接段错误崩溃)中展现出完全不同的运行结果。 - [00:00](https://www.youtube.com/watch?v=ltCNkcl9HGI&t=0s) 在 Python 3.11 及以前,列表推导式会生成独立的 code object 并像临时函数一样被调用,迭代器被存放在名为 '.0' 的特殊局部变量中。 - [00:00](https://www.youtube.com/watch?v=ltCNkcl9HGI&t=0s) Python 3.12 实施了 PEP 709(推导式内联优化),让推导式直接在外层函数的栈帧(frame)内原地执行,避免了高昂的函数调用开销。 - [00:00](https://www.youtube.com/watch?v=ltCNkcl9HGI&t=0s) 为保证作用域隔离,PEP 709 引入了字节码指令 LOAD_FAST_AND_CLEAR,在内联推导式执行前暂存并清空同名局部变量槽位。 - [05:00](https://www.youtube.com/watch?v=ltCNkcl9HGI&t=300s) 代码中未执行的匿名函数 lambda: k 会在编译期将变量 k 静态标记为闭包变量,并在函数启动时由 MAKE_CELL 封装为 cell object。 - [05:00](https://www.youtube.com/watch?v=ltCNkcl9HGI&t=300s) 内联推导式模拟独立函数语义时,将原本存放 cell object 的变量槽覆盖写入了纯数字对象(非 cell 对象)。 - [05:00](https://www.youtube.com/watch?v=ltCNkcl9HGI&t=300s) Python 3.13 引入的 PEP 667 重构了 locals() 机制,底层的 _PyFrame_LocalsProxy_GetVal 依靠 code object 静态属性判定变量是否属于闭包。 - [05:00](https://www.youtube.com/watch?v=ltCNkcl9HGI&t=300s) 静态判定与动态内联产生根本冲突:解释器坚信 k 是闭包并调用 PyCell_Get 强行解包,最终因操作非 cell 对象触发断言失败和段错误。 - [05:00](https://www.youtube.com/watch?v=ltCNkcl9HGI&t=300s) 高天提交的修复补丁在 C 语言层增加了动态类型校验,确保对象确为 cell object 时才进行解包,否则直接返回原值。 - [05:00](https://www.youtube.com/watch?v=ltCNkcl9HGI&t=300s) 该修复方案展示了典型的工业界权衡:无法在不重构解释器架构的前提下完全覆盖所有极端 corner case,但消除崩溃让系统‘绝对更好’本身就具有极高工程价值。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=ltCNkcl9HGI&t=0s) 五行神秘代码与跨版本行为差异:PEP 709 的内联推导式机制

视频开篇展示了一段仅有五行的 Python 程序,并提出了一个看似简单实则极为刁钻的提问:这段代码运行后到底会打印出什么?对此,码农高天直言:“这道题我不认为世界上有任何一个人可以完整的答出来。你如果不信的话可以暂停一下,然后写出你的答案。”紧接着公布的真实运行结果令人大跌眼镜,不同 Python 版本之间展现出了完全不同的行为:

| Python 版本 | 运行结果 / 行为表现 | |:--- |:--- | | **Python 3.11 及更早版本** | 输出字典包含 `'.0'`(对应一个 iterator)以及 `'k': 0` | | **Python 3.12** | 字典中仅有 `'k': 0`,没有了特殊键 `'.0'` | | **Python 3.13** | 直接触发段错误崩溃(Segmentation fault,简称 sigfault) |

这段代码中包含了一个列表推导式(List Comprehension)。在 Python 3.11 及以前,列表推导式会新建一个单独的 `code object`,其运行方式就如同在后台悄悄生成并调用了一个独立的临时函数。该函数为了迭代外部传入的序列,会把传入的 iterable 存入一个具有特殊命名格式的局部变量中,即 `'.0'`。然而,重新创建函数并进行调用的开销较大,运行效率偏低。

为了优化性能,Python 3.12 引入了 PEP 709(Inline Comprehensions,推导式内联优化)。在 3.12 中,推导式不再创建新的 `code object` 去发起函数调用,而是直接在当前函数的栈帧(frame)内执行。但从用户语义上看,推导式内部的局部变量依然必须与外部的局部变量相互隔离。例如即使推导式内部和外部函数的局部变量名都叫 `k`,推导式迭代结束后外部的 `k` 依然不能被篡改。在字节码(bytecode)层面上,CPython 借助 `LOAD_FAST_AND_CLEAR` 实现了这一隔离:该指令先把原有的局部变量 `k` 暂存起来,并将该槽位清空;随后推导式运行时的 `STORE_FAST` 便能安全地向局部变量 `k` 写入新值而不污染原数据。同时,由于推导式已直接内联到当前 frame,运行时可以直接去索引列表对象而无需再借助临时变量存储迭代器,这就解释了为何 Python 3.12 的 `locals()` 结果中再也见不到 `'.0'`。

```mermaid flowchart TD accTitle: PEP 709 推导式运行机制对比 accDescr: 对比 Python 3.11 以前创建临时函数与 Python 3.12 内联推导式的栈帧差异。

subgraph Legacy["Python 3.11 及更早版本"] A["外层函数 Frame"] -->|"创建并调用新函数"| B["推导式独立 Code Object"] B -->|"参数存为特殊变量"| C["'.0' (Iterator)"] end

subgraph Inline["Python 3.12 (PEP 709)"] D["外层函数 Frame"] -->|"原地展开执行"| E["Inline Comprehension"] E -->|"LOAD_FAST_AND_CLEAR"| F["暂存并隔离原有变量 k"] E -->|"直接访问源列表"| G["无需生成临时变量 '.0'"] end ```

---

[05:00](https://www.youtube.com/watch?v=ltCNkcl9HGI&t=300s) 闭包静态判定与动态内联的冲突:PEP 667 引发的崩溃与修复妥协

列表推导式的内联机制在 Python 3.12 与 3.13 中并没有原则变化,导致 3.13 发生 Segmentation fault 的真正祸首是 PEP 667 对 `locals()` 的底层机制重构。码农高天调侃道:“如果大家有印象的话,我之前提到过,有一个大傻子做了 PEP 667,它改变了 locals 的运行机制,就是这个改动啊,直接把 Python 3.13 给 crash 了。”

在原代码的第二行,写着一个看似毫无作用、既未调用也未保存的匿名函数 `lambda: k`。正是这行代码,让变量 `k` 从一个普通的局部变量变成了被闭包捕获的闭包变量。在 CPython 中,闭包变量在底层是由 `cell object` 负责包装存储的。当函数启动时,字节码会调用 `MAKE_CELL` 将 `k` 转化为 cell object。然而,当程序执行到内联推导式时,为了模拟独立函数的语义,推导式内部循环写入的 `k` 应该表现为纯粹的普通局部变量(fast variable)。因此,`LOAD_FAST_AND_CLEAR` 将保存有 cell object 的闭包变量 `k` 偷偷暂存并清空,推导式随后直接将单纯的数值 `0` 通过 `STORE_FAST` 写回变量槽位,此时变量槽内只存着一个数值对象,根本没有包装任何 cell object。

崩溃的导火索就在此时发生:推导式内部紧接着调用了 `locals()`。在 PEP 667 引入的底层函数 `_PyFrame_LocalsProxy_GetVal` 中,解释器需要依据 `code object` 静态记录的信息来判断某个位置的变量是否属于闭包变量。如果被标记为闭包变量,它就会调用 `PyCell_Get` 强行把底层的 cell object 打开并取值。由于推导式被内联,它与外层函数共享同一个 `code object`;编译器在静态编译期看到 `lambda: k` 时,就断定 `k` 是一个闭包变量。但处于内联推导式中时,真正的闭包已经被挪走,变量槽中实际上只是一个整数 `0`。当代码试图对一个非 cell 对象强行解包时,断言失败,直接在底层引发了段错误。

针对这个 bug,核心矛盾在于:“CodeObject 认为一个变量是不是闭包变量是静态的,这件事是在编译期决定的;而由于 inline comprehension 的引进,这个变量是不是闭包变量其实是动态的,有时候是有,有时候不是。你用一个静态的标准去判断一个动态的变量,当然就会出问题。”

为了修复此崩溃,高天提交的补丁在静态判定之后补上了一层动态防御检查:检查该变量所保存的对象是否真的是一个 cell object;如果不是,就直接返回该值而不强行拆包。但这又引出了 Python 过于灵活所带来的死角:用户可以在 Python 中手动实例化一个 `cell_type` 对象并赋予普通局部变量。面对这种变量本身是普通变量、内部恰好保存了一个 cell object 的极端边界情况(corner case),当前的 CPython 机制在不重写编译器或解释器架构的前提下根本无法区分。尽管留下了边界瑕疵,由于段错误是性质最恶劣的崩溃级缺陷,该补丁仍顺利被官方合并。码农高天由此总结道,在实际的工业界开发中,完美并非总是首要指标:“在工厂里这个也非常常见,你的工作并不是要把一个问题解决得尽善尽美它才有价值,绝对更好已经是一个非常了不起的成就了。”

- Python 的静态编译特性(编译阶段根据 AST 预先生成静态的变量作用域属性)与推导式内联(运行时同一栈帧内动态切换变量的局部/闭包形态)产生了根本性冲突。 - 在 Debug 版本的 Python 3.13 源码中,该错误会在执行 `PyCell_Get` 相关的断言处直接触发 Assertion Error,明确提示该 value 并非 cell。 - 最终的修复方案属于实用主义权衡:在 C 语言层增加动态类型校验,消除了致命的 Segmentation fault,将破坏系统的 crash 降级为一个极难触碰的语义边界情况。

## Notable quotes > [05:00](https://www.youtube.com/watch?v=ltCNkcl9HGI&t=300s) 你的工作并不是要把一个问题解决得尽善尽美它才有价值,绝对更好已经是一个非常了不起的成就了。 — 码农高天

## Mentioned Python, 码农高天, PEP 709, PEP 667, CPython

## Video description 短短的五行程序都猜不到结果?这里面可有不少学问。从PEP 709到PEP 667,多少个改动凑到一起才触发了这个特殊的案例?

[Source](https://www.youtube.com/watch?v=ltCNkcl9HGI)

中文简报

Python 3.13修复推导式内联与闭包判定冲突导致的段错误 PEP 667依赖静态编译信息解析locals,遇到PEP 709动态剥离闭包变量的内联推导式时会强行解包非cell对象,最终通过在C语言层补充类型检查消除崩溃。 视频深入剖析了一段仅有五行却在不同 Python 版本中表现截然不同的代码:在 Python 3.11 及更早版本中会输出包含迭代器临时变量的字典,在 3.12 中得益于 PEP 709 的推导式内联优化移除了临时变量,而在 3.13 中则因 PEP 667 对 locals 机制的重构直接导致了段错误崩溃。崩溃的根本原因在于静态编译期对闭包变量的判定与运行时内联推导式动态复用变量槽位之间的冲突,导致解释器尝试对普通整数执行解包操作。码农高天详细解释了底层的字节码运行逻辑,并分享了自己在 CPython 源码中提交动态校验补丁的修复过程与工程权衡。 一段仅有五行的 Python 代码在跨版本执行时表现大相径庭:Python 3.11 及更早版本字典输出包含特殊变量 '.0',3.12 仅保留普通局部变量,而 3.13 则会直接因段错误(Segmentation fault)崩溃。 (0:00) Python 3.12 的 PEP 709 引入了推导式内联(Inline Comprehensions),取消了原先独立创建 code object 的函数调用开销,改用 LOAD_FAST_AND_CLEAR 在当前栈帧内实现变量隔离。 (0:00) Python 3.13 崩溃的主因是 PEP 667 重构 locals() 时,解释器依靠静态编译期信息判定闭包变量,却在运行时遇到了内联推导式动态写入的非 cell 对象,导致强行解包引发段错误。 (5:00) 官方最终通过在 C 层增加动态类型检查修复了该崩溃,尽管在手动传入 cell_type 的极端边界情况下仍存在理论瑕疵,但在工程权衡中成功消除了致命的段错误缺陷。 (5:00)

Reading summary

码农高天指出,一段包含闭包与推导式的五行 Python 代码在不同版本中表现迥异,甚至在 Python 3.13 中触发段错误崩溃;其根本原因在于 PEP 709 的内联推导式机制使变量作用域产生动态切换,与 PEP 667 针对 locals() 的编译期静态闭包判定发生了底层冲突。 跨版本行为分歧与 PEP 709 推导式内联 (0:00) 该五行程序在不同 Python 版本中的执行结果截然不同:Python 3.11 及更早版本会在字典中返回代表迭代器的特殊变量 '.0' 以及 'k': 0;Python 3.12 仅保留 'k': 0;Python 3.13 则直接导致段错误(Segmentation fault)崩溃。在 3.11 之前,列表推导式会通过创建独立的 code object 作为临时函数调用,需要将传入的 iterable 存放于 '.0' 局部变量中,运行开销较大。 Python 3.12 引入了 PEP 709(推导式内联优化),推导式不再新建栈帧而是直接在当前函数的 frame 内原地展开。为了维持变量作用域隔离,CPython 使用 LOAD_FAST_AND_CLEAR 指令将原有的局部变量 k 暂存并清空该槽位,推导式内部随后通过 STORE_FAST 写入新值;同时由于内联后可直接索引源列表,运行时不再生成临时变量 '.0',从而改变了 locals() 的键值结构。 静态闭包判定与动态内联冲突诱发崩溃 (5:00) 导致 Python 3.13 崩溃的诱因是 PEP 667 对 locals() 底层机制的重构。代码中存在未被调用的匿名函数 lambda: k,导致变量 k 在静态编译阶段被判定为闭包变量,底层在函数启动时通过 MAKE_CELL 指令将其包装为 cell object。然而在内联推导式执行期间,LOAD_FAST_AND_CLEAR 将原本存有 cell object 的槽位清空并由推导式循环直接写入纯整数值 0,使该槽位暂时退化为普通局部变量。 推导式内部调用 locals() 时,PEP 667 引入的底层函数 _PyFrame_LocalsProxy_GetVal 依据外层共享 code object 的静态编译记录,断定 k 仍是闭包变量,遂调用 PyCell_Get 强行解包取值。由于槽位中此时仅有整数 0 而非 cell object,导致断言失败并在底层引发段错误。这暴露了 CodeObject 依赖编译期 AST 作出静态闭包判定,与推导式内联带来的运行时动态变量形态之间的根本冲突。 C 层动态防御与工程修复妥协 (5:00) 为消除该崩溃缺陷,码农高天提交的修复补丁在 _PyFrame_LocalsProxy_GetVal 的静态判定后增加了一层动态类型检查:若变量槽内的对象并非真正的 cell object,则直接返回该值而不执行强行拆包。该方案消除了破坏性最强的 Segmentation fault,并已被官方合并。 该修复同时伴随边缘语义瑕疵。由于 Python 允许用户手动实例化 cell_type 对象并赋给普通局部变量,在现有 CPython 架构下,解释器无法在完全不重构编译器和运行时机制的前提下区分“存储了 cell 对象的普通变量”与“真正的闭包 cell”。工程实践上,开发团队选择了以动态检查解决系统崩溃、降级缺陷严重程度的实用主义权衡。 该案例表明,编译期静态作用域判定与运行期动态栈帧优化之间存在架构张力;在不重写解释器底层结构的前提下,通过增加动态防御检查消除段错误,是工业级语言演进中接受边界瑕疵的实用妥协。

Source quotes

[5:00] 码农高天: 你的工作并不是要把一个问题解决得尽善尽美它才有价值,绝对更好已经是一个非常了不起的成就了。

Original transcript

[0:00] 屏幕左边有五行拍档代码请问打印出来的是什么这道题我不认为世界上有任何一个人可以完整的答出来你如果不信的话可以暂停一下然后写出你的答案正确答案是这样的在拍档3.11和之前的版本中是.0对应着一个iterator,然后k是0,在python3.12中k是0,而在python3.13中是sig fault这个答案你答对了吗我们今天就来聊一下这背后到底发生了什么Python 3.11和Python 3.12最大的区别是Python 709,也就是Inline Comprehensions,我们看这个东西就叫comprehensions在3.11和之前的版本里这个comprehension会新建一个code object你可以理解为他会偷偷的重新建立一个函数然后来调用这个函数在这个函数里你的list comprehension的那个iterable被保存到了一个特殊名字的变量里也就是我们刚才看到的那个点0但是大家可以想象重新建立一个函数并且进行函数调用它的效率是偏低的这也正是为什么我们要试图inline comprehension我们希望这个过程不需要调用一个新的函数于是在python3.12里我们的这种 least comprehension 就不再新建一个code object 然后再调用那个函数了而是直接在当前函数的frame下来解决这个问题但是我们依然希望从用户的角度看,这个List Comprehension跟之前的List Comprehension行为是一致的在这个例子里,尽管我们的 list comprehension 和 local variable 里面都有一个 k,我们仍然不希望这个K在进行完List Comprehension之后只有所改变也就是如果你想象这个list comprehension是一个新的函数那这个k就是那个函数里面的一个local variable那在一个函数里面改变local variable怎么可能会影响另外一个函数的local variable呢所以它的实现一定要保证K这个变量在进行完list comprehension之后还是1那在bytecode上面具体的核心操作就是这个load fast and clear它大概的意思就是我把k这个变量先给偷偷保存起来然后把这个变量删掉这样你在list comprehension里面再对我这个k变量做改变的时候就不会影响我之前保存的那个变量大家可以看到32和44这两个by code操作的是一个变量他们这个参数都是0都是k这个变量只不过32这个load fast and clear把之前的那个K变量给保存起来了这样44再往k变量里面存东西的时候就不影响之前那由于有了这个inline机制的存在所以我们在打印local的时候就没有那个点0了因为我们不再需要一个特殊的变量去保存这个iterable我们可以直接去找这个方括号0从那里面拿东西就行了这就解释了为什么3.12和3.11相比少了一个.0的那个key那python3.13为什么塞个fault这才是我们今天的重点Inline Comprehension的机制从3.12到3.13并没有变化变了的东西是locos如果大家有印象的话,我之前提到过,有一个大傻子做了PIP667它改变了Locos的运行机制就是这个改动啊直接把python3.13给crush了我们回头来看这个程序这第二行显得尤为诡异因为他看似完全没有参与程序的运行这是一个单独defined lambda 函数我们甚至没有把这个LAM的函数给保存起来但是如果你看了我们之前的视频你就应该能意识到这一行有一个非常重要的作用就是把k这个变量从一个普通的局部变量变成了一个臂包变量第二行是一个小小的臂包它相当于建立了一个新的函数然后在这个函数里面返回了K于是在这两行的共同作用下K成为了一个币包变量而我们之前介绍过在Cpython中实际保存臂包变量的是一个cell object但是我们把注意力再放到这个 least comprehension 上在这个least comprehension里K是一个币包变量吗我们说过这个least comprehension是要模仿单独建立一个函数时候的状态我们看如果写成这个形式你认为G里面是个K是B包变量吗它不是一个臂包变量因为他被写入了K是一个正儿八经的局部变量也就是说在这个函数G里这个k就是一个普通的fast variable它并不是保存在cell object里面的同样的 [5:00] 在comprehension里面希望起到效果也是k就是一个普通的fast variable那我们看一下这个Bitcode这个函数上来把这个K make cell从这个时候开始这个K就是一个cell object或者说它是一个臂包变量然而在下面做这个load fast and clear的时候我们把这个臂包变量偷偷保存了起来然后把这个变量删掉了在这个comprehension里面我们从list 0里面拿了一个东西然后直接做store fast存到了k里这个时候的k里面保存的是一个单纯的0没有sell object紧接着我们靠了locals而在某个大聪明写的这个locos代码里用到了这个frame_locals_proxy_gap_valve这个函数是根据当前的code_object和这个变量对应的位置来拿到这个变量的值如果这个变量是一个币包变量的话你要把它从 cell object 里面拿出来而这个变量是不是臂包变量是保存在这个code object里的这个code object在建立的时候就知道每一个变量它是不是臂包变量我们就用这个code object给的信息如果这个变量是一个币包变量的话我们就把这个值从cell里给拿出来在这里大家发现问题了吗由于做了inline comprehension我们这个comprehension和这个函数f其实是同一个code object而这个code object当然认为k是一个臂包面量因为在compile的时候他已经发现了这会有个lambda k可是这个b包变量已经被我们偷偷的藏起来了当我们在这个comprehension里面的时候这个k它不是一个币包变量了那这个V6明明不是一个cell你还要强行从这里面做一个拍cellget想从里面拿点东西出来那他肯定就塞个抱上了对吧实际上如果你compile的是debug version的话38行assertion是会出问题的他会告诉你你这个value啊他不是一个cell那从本质上说这个问题的核心就是CodeObject认为一个变量是不是臂包变量是静态的这件事是在编译期决定的而由于inline comprehension的引进这个变量是不是臂包变量其实是动态的有时候是有时候不是你用一个静态的标准去判断一个动态的变量当然就会出问题那我们找到问题的根源了接下来怎么办很好玩对吧我们只要在这里面再检查一下这个东西是不是cell就好了吗如果它里面保存的确实是一个cell object,那我们就从里面拿,如果不是的话,拉倒,我直接把这个值给你不就得了吗?这样我们就在静态的基础上增加了一个动态的检查一切都看起来很美好是不是但是Python这个语言它太自由了它允许你自己建立一个cell object我们看这里现在在做local时的时候K是作为一个普通局部变量保存着一个CellObject里面放的1而不是作为一个臂包变量所以本身是cell type可我们希望它表现成了一个单纯的1在cpython的level这两个东西是一模一样的而这一段函数我们在打印的时候由于我们刚才的那个改动他会认为k对应的就是1可实际上这个K打印出来应该是下面这个玩意它应该是一个cell object我们并没有考虑这个变量是个普通局部变量只是里面恰好保存一个cell object的情况在这种情况下我们是不应该把这个cell给打开的那问题就来了怎么办呢?很遗憾的告诉大家没辙没辙在现在的C Python机制下我们没有办法区分这二者如果想要区分的话,我们需要去改compiler或者interpreter那就是一个非常大的修改了但是回到我们这个fix我们这个bug fix并没有让事情变得更糟也就是一个普通变量里面保存cell object 情况之前也是没法解决的我们的修改虽然没有完全解决这个问题但是让这个问题变得绝对更好了而且这是一个很大的进展因为sack fault是很严重的事情所以尽管我们留了一个corner case我们这个bug fix还是成功的merge了因为这个patch是非常有价值的那在工厂里这个也非常常见你的工作并不是要把一个问题解决的尽善尽美他才有价值绝对更好已经是一个非常了不起的成就了好虽然这期视频的内容绝大部分人一辈子也用不上但如果你喜欢这样内容的话记得点关注有点关注三连字幕志愿者 杨茜茜