CPython采纳欺骗编译器方案 修复pdb十余年闭包Bug

【python】这个十多年的bug,没点黑魔法还真解决不了 — 码农高天

码农高天

由于exec无法感知外层作用域导致匿名函数回退全局查找,CPython通过构造虚拟外层函数迫使编译器生成闭包字节码并手动注入局部变量,从底层终结了pdb长期的交互报错。

AI-assisted summary of 【python】这个十多年的bug,没点黑魔法还真解决不了 — original source. Check the original for full context.

Source published:

Key ideas

  • CPython 的 pdb 存在一个十余年的陈年 Bug:在调试交互环境中通过 exec 执行包含闭包的代码(如列表推导式、lambda)时,匿名函数无法感知外层局部变量,会退回到全局作用域并抛出 NameError。 (0:00)
  • Python 3.12 引入 PEP 709 内联了推导式,但未根治问题;任何会生成新 Code Object 的语法(如 lambda 表达式)依然会因全局变量回退而报错。 (0:00)
  • 码农高天提出“欺骗编译器”思路,动态构造外层虚拟函数诱导编译器生成带有 LOAD_DEREF 的闭包 Code Object,再注入真实的调用栈局部变量执行。 (5:00)
  • 该方案以核心函数 exec_in_closure 落地合入 CPython,仅用 40 余行有效代码便从 Python 3.13 起彻底修复了该问题。 (7:20)

Detailed notes

## Background 该视频由 YouTube 频道“码农高天”于 2024 年 7 月 6 日发布。视频简介说明,作者分享了自己当年为 CPython 修复的一个陈年 Bug;该问题虽被多次上报,但因被普遍认为修复难度极大而长期搁置,视频展示了如何运用“黑魔法”技巧成功攻克这一难题。

## TL;DR 视频深入剖析了 CPython 内置调试器 pdb 中困扰社区十余年的闭包变量 NameError 缺陷。该问题的根因在于 pdb 通过 exec 动态执行代码时,新生成的匿名函数或 lambda 会丢失外层函数上下文,错误地使用 LOAD_GLOBAL 字节码寻找变量而非走闭包通道。作者提出了一种“欺骗编译器”的巧妙思路,通过构造外层嵌套函数迫使编译器生成带有闭包引用的 code object,并辅以底层闭包单元注入和显式写回机制。该方案最终凝练为仅约 40 行核心代码的 exec_in_closure 函数,并从 Python 3.13 开始正式并入 CPython。

## Atmosphere & style 整体基调硬核、严谨而充满黑客极客的成就感。码农高天以抽丝剥茧的逻辑层层拆解底层编译与执行细节,语气自信从容、通俗易懂;当展示用 40 余行“黑魔法”代码轻巧化解十余年历史顽疾时,洋溢着顶级开源贡献者对语言机制了然于胸的技术自豪感。

## Bulletin - [00:00](https://www.youtube.com/watch?v=Mp_f1sBckjU&t=0s) CPython 的 pdb 存在一个十余年的陈年 Bug:在调试交互环境中通过 exec 执行包含闭包的代码(如列表推导式、lambda)时,匿名函数无法感知外层局部变量,会退回到全局作用域并抛出 NameError。 - [00:00](https://www.youtube.com/watch?v=Mp_f1sBckjU&t=0s) Python 3.12 引入 PEP 709 内联了推导式,但未根治问题;任何会生成新 Code Object 的语法(如 lambda 表达式)依然会因全局变量回退而报错。 - [05:00](https://www.youtube.com/watch?v=Mp_f1sBckjU&t=300s) 码农高天提出“欺骗编译器”思路,动态构造外层虚拟函数诱导编译器生成带有 LOAD_DEREF 的闭包 Code Object,再注入真实的调用栈局部变量执行。 - [07:20](https://www.youtube.com/watch?v=Mp_f1sBckjU&t=440s) 该方案以核心函数 exec_in_closure 落地合入 CPython,仅用 40 余行有效代码便从 Python 3.13 起彻底修复了该问题。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=Mp_f1sBckjU&t=0s) 调试器(pdb)是 Python 中最适合且必须使用黑魔法的特殊场景。 - [00:00](https://www.youtube.com/watch?v=Mp_f1sBckjU&t=0s) 该闭包访问失效 Bug 在 Python 社区被报告至少 5 次,但因涉及 exec 底层语义难以修改而被长期搁置。 - [00:00](https://www.youtube.com/watch?v=Mp_f1sBckjU&t=0s) 正常编译时,嵌套匿名函数通过 LOAD_DEREF 字节码读取外层局部变量;而 pdb 动态通过 exec 运行独立字符串时,编译器会退化生成 LOAD_GLOBAL 指令。 - [00:00](https://www.youtube.com/watch?v=Mp_f1sBckjU&t=0s) PEP 709(Comprehension Inlining)内联推导式仅表面掩盖了推导式报错,底层生成 Code Object 的语法(如 lambda: n)仍会崩溃。 - [05:00](https://www.youtube.com/watch?v=Mp_f1sBckjU&t=300s) 由于无法更改 exec 的设计,核心解决思路是让编译器“以为”用户输入的代码本就处于包含相应变量的外层函数闭包中。 - [05:00](https://www.youtube.com/watch?v=Mp_f1sBckjU&t=300s) 通过构造 outer/inner 模板并在 outer 中定义虚拟占位变量(如 n = 0),即可诱导编译器生成具备闭包引用能力的内层 Code Object。 - [05:00](https://www.youtube.com/watch?v=Mp_f1sBckjU&t=300s) 虚拟外层占位变量仅用于通过编译期的符号解析,运行时会直接由调试器注入当前断点帧(Frame)的真实局部变量单元。 - [07:20](https://www.youtube.com/watch?v=Mp_f1sBckjU&t=440s) 核心函数 exec_in_closure 首先扫描 compile 结果的 co_consts,判定用户输入是否包含嵌套 Code Object 以决定是否走原生路径。 - [07:20](https://www.youtube.com/watch?v=Mp_f1sBckjU&t=440s) 函数通过区分 Expression 与 Statement 行为,确保表达式能正确打印计算结果,而语句不会输出多余返回值。 - [07:20](https://www.youtube.com/watch?v=Mp_f1sBckjU&t=440s) 执行环境采用了隔离命名空间并在执行后显式写回(Writeback)原 frame 局部变量字典的机制,兼顾安全性与调试便利性。 - [07:20](https://www.youtube.com/watch?v=Mp_f1sBckjU&t=440s) 该修复仅用 40 多行精巧的底层核心代码完成,自 Python 3.13 起正式成为 CPython 标准库实现。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=Mp_f1sBckjU&t=0s) PDB 中十余年的闭包变量报错 Bug 与根因剖析

程序员在编写程序时通常应尽量避免使用“黑魔法”,但在 Python 的调试器(debugger)场景下,黑魔法不仅不可或缺,而且是最适合应用的地方。码农高天在 CPython 的 pdb 中引入了一段他最喜欢的黑魔法实现,用以攻坚一个困扰社区十余年、被提出过至少 5 次 issue 却因“太难修”而长期搁置的陈年 bug。

```mermaid flowchart TD accTitle: PDB 执行代码时局部变量与闭包冲突机制 accDescr: 对比正常函数编译时的 LOAD_DEREF 与 exec 运行时的 LOAD_GLOBAL

subgraph Normal["正常编译环境(如 Python 3.11 及之前)"] A["局部变量 n = 2"] --> B["List Comprehension 匿名函数"] B --> C["编译器识别外层局部变量"] C --> D["生成字节码 LOAD_DEREF<br/>通过闭包(Closure)传递 n"] end

subgraph PDBExec["PDB / exec 动态执行环境"] E["用户输入字符串: print([i for i in...])"] --> F["exec(source, globals, locals)"] F --> G["动态生成匿名函数 Code Object"] G --> H["独立字符串上下文无外层函数语义"] H --> I["生成字节码 LOAD_GLOBAL<br/>直接往全局变量找 n"] I --> J["抛出 NameError: name 'n' is not defined"] end ```

该问题的典型复现代码非常常见:在一个函数 `f` 中定义局部变量 `n = 2`,随后通过列表推导式(List Comprehension)筛选 0 到 9 中能被 `n` 整除的数并打印(`print([x for x in range(10) if x % n == 0])`)。正常执行会输出 `[0, 2, 4, 6, 8]`。然而,如果在函数内部打断点进入 pdb,并原封不动地将这行 `print` 代码复制到交互界面中运行,pdb 就会抛出 `NameError`,提示找不到 `n`;但若直接在 pdb 中输入 `print(n)`,它又能正确打印出 `2`。

通过 `dis` 反汇编字节码可以看清本质:在 Python 3.11 之前,列表推导式在底层会建立一个独立的匿名函数并执行。正常编译时,编译器知道 `n` 是外层函数的局部变量,因此在匿名函数的 code object 中读取 `n` 使用的是 `LOAD_DEREF` 字节码指令,通过闭包机制进行变量传递。而在 pdb 内部,用户输入的代码实际上是通过 `exec` 执行的字符串。`exec` 无法感知外层调用栈的函数上下文,其新建的匿名函数内部使用 `LOAD_GLOBAL` 寻找变量;发现局部变量中没有 `n` 时,它直接回退到全局命名空间寻找,导致触发 `NameError`。

- 尽管 Python 3.12 引入了 PEP 709(Comprehension Inlining),列表推导式和字典推导式被直接内联展开、不再创建新的 code object(改为使用 `LOAD_NAME` 直接在传入的 `locals` 中查找),使得列表推导式的报错表面上消失了。 - 根因并未解决:任何其他会生成新 code object 的语法(例如 lambda 表达式 `(lambda: n)()`)在通过 `exec` 或在老版本 pdb 运行时,依然会因尝试查找全局变量而崩溃抛出 `NameError`。

---

[05:00](https://www.youtube.com/watch?v=Mp_f1sBckjU&t=300s) 欺骗编译器:构造 Outer/Inner 结构获取闭包 Code Object

“我们是没有办法改变 exec 这件事的,不现实,但是我们有没有可能让它在 pdb 里面成功的运行呢?当然是可能的。”由于无法强行修改底层匿名函数生成的变量查找机制,码农高天提出的思路是“欺骗编译器”:既然代码自己创建的匿名函数无法手工注入局部命名空间,那就让编译器误以为这段代码本身就处于闭包之中,从而迫使编译器生成带有闭包引用(`LOAD_DEREF`)的 code object,再由调试器把变量注入闭包单元中执行。

```mermaid flowchart LR accTitle: 欺骗编译器提取闭包 Code Object 流程 accDescr: 展示如何用伪造外层函数骗过编译器并提取内层闭包代码对象

A["用户代码字符串:<br/>lambda: n"] --> B["构造欺骗模板:<br/>def _outer():<br/> n = 0<br/> def _inner(): return (lambda: n)<br/> return _inner.__code__"] B --> C["执行 outer 定义并调用"] C --> D["获得内层 _inner 的 Code Object"] D --> E["从当前 Frame locals 提取 n 的值注入 Closure Tuple"] E --> F["执行 Code Object 得到正确结果 2"] ```

具体做法是构造一段包装代码,在外部包裹一个 `outer` 函数,并将用户输入的代码(如 `lambda: n`)放入其内部的 `inner` 函数中。在 `outer` 内部人为定义一个虚拟的 `n = 0`,编译器在编译整段字符串时,便会判定 `inner` 内部的 `n` 来自于 `outer`,从而为 `inner` 构造出具备闭包变量访问权限的 code object。这个在外层定义的 `n = 0` 纯粹是为了骗过编译期的符号分析,与最终运行时绑定的实际值无关;外层结构在取得目标的 `__code__` 后即可丢弃。

“我们要让编译器以为这一段本身就需要闭包,然后建立一个用闭包的 code object,最后去执行那个 code object,而不是我们给的这个语句本身。”通过这种方式提取出内层 code object,调试器只需在调用时将真实的局部变量填入闭包单元(closure cell),便能驱动原本执行失败的代码。在演示示例中,即便在外层代码中写死 `n = 0`,在注入当前的局部变量后执行,依然成功打印出断点环境中的真实值 `2`。

---

[07:20](https://www.youtube.com/watch?v=Mp_f1sBckjU&t=440s) CPython pdb 的 `exec_in_closure` 黑魔法源码与实现闭环

这项技术方案最终在 CPython 的 pdb 中以核心函数 `exec_in_closure` 落地。该函数接收源码字符串 `source`、`globals` 以及 `locals` 命名空间,通过一系列精细的底层操作完成执行:

1. **检查是否产生 Code Object**:首先调用 `compile` 将传入的 `source` 编译成 code object,并遍历其常量元组(`co_consts`)。通过检查这些常量中是否包含 `code` 类型的对象,来判定用户代码是否定义了函数、lambda 或推导式。若没有产生任何新的 code object,说明是普通扁平代码,直接按照原生方式在当前命名空间执行即可。 2. **区分 Expression 与 Statement**:若用户输入的是表达式(如 `x + 1`),预期的交互行为是打印结果;若输入的是语句(如 `y = x + 1`),则不应打印任何输出。函数会检测输入类型,并在需要时通过拼接代码将表达式包裹,利用专用变量 `_pdb_eval_result` 捕获返回值,最后视其是否为 `None` 决定是否输出。 3. **隔离命名空间与显式写回(Writeback)**:为了防止调试执行意外污染调用栈环境,函数会对局部变量命名空间进行复制。但由于用户在调试器输入 `x = 1` 时确实希望断点所在函数的外层局部变量生效,系统记录了运行产生变更的局部变量,并在执行完毕后显式地写回原 frame 的局部变量字典中。 4. **编译外层欺骗代码并提取 Code Object**:动态生成包含外层伪造作用域的源码,执行其定义以获取内层包含闭包机制的真实 code object。 5. **组装闭包元组(Closure Tuple)并执行**:将断点当前帧的所有局部变量统统构造进闭包单元元组中,把它们作为闭包环境传递给前面提取出来的 code object 并启动运行。代码成功获取依赖的变量值,完成计算后写回变量并打印结果。

“这个函数刨去注释与空行一共只有 40 多行有效代码,但是在这 40 多行有效代码里面充满了跟 Python 有关的黑魔法。”码农高天表示,这段精巧的实现从 Python 3.13 开始正式随 CPython 发布。如果有人轻视 Python 认为这门语言过于简单,大可将这段 40 余行的 `exec_in_closure` 发给对方,检验其是否能完全参透其背后的编译与执行黑魔法。

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

## Video description 这期视频和大家分享一个我今年给CPython改的bug,这个bug被report了很多次,一直没人修,因为大家觉得这个问题太难修了。看看我怎么用黑魔法来解决这个问题的吧!

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

中文简报

CPython采纳欺骗编译器方案 修复pdb十余年闭包Bug 由于exec无法感知外层作用域导致匿名函数回退全局查找,CPython通过构造虚拟外层函数迫使编译器生成闭包字节码并手动注入局部变量,从底层终结了pdb长期的交互报错。 视频深入剖析了 CPython 内置调试器 pdb 中困扰社区十余年的闭包变量 NameError 缺陷。该问题的根因在于 pdb 通过 exec 动态执行代码时,新生成的匿名函数或 lambda 会丢失外层函数上下文,错误地使用 LOAD_GLOBAL 字节码寻找变量而非走闭包通道。作者提出了一种“欺骗编译器”的巧妙思路,通过构造外层嵌套函数迫使编译器生成带有闭包引用的 code object,并辅以底层闭包单元注入和显式写回机制。该方案最终凝练为仅约 40 行核心代码的 exec_in_closure 函数,并从 Python 3.13 开始正式并入 CPython。 CPython 的 pdb 存在一个十余年的陈年 Bug:在调试交互环境中通过 exec 执行包含闭包的代码(如列表推导式、lambda)时,匿名函数无法感知外层局部变量,会退回到全局作用域并抛出 NameError。 (0:00) Python 3.12 引入 PEP 709 内联了推导式,但未根治问题;任何会生成新 Code Object 的语法(如 lambda 表达式)依然会因全局变量回退而报错。 (0:00) 码农高天提出“欺骗编译器”思路,动态构造外层虚拟函数诱导编译器生成带有 LOAD_DEREF 的闭包 Code Object,再注入真实的调用栈局部变量执行。 (5:00) 该方案以核心函数 exec_in_closure 落地合入 CPython,仅用 40 余行有效代码便从 Python 3.13 起彻底修复了该问题。 (7:20)

Reading summary

码农高天解析了 Python 调试器(pdb)中困扰社区十余年、涉及匿名作用域变量查找失败的陈年 bug,并详细介绍了通过欺骗编译器生成闭包 Code Object 的“黑魔法”修补方案,该方案已在 Python 3.13 正式合入 CPython。 PDB 闭包变量 NameError 的底层根因 (0:00) 在 Python 调试器中,如果在包含局部变量 n = 2 的函数内断点,并在 pdb 中执行如列表推导式 print([x for x in range(10) if x % n == 0]),会触发 NameError 报错找不到 n,但在 pdb 中直接输入 print(n) 却能正常输出。在 Python 3.11 及之前,列表推导式在底层会建立独立的匿名函数。在普通编译环境下,编译器识别到外层局部变量,会在字节码中使用 LOAD_DEREF 通过闭包传递变量;但在 pdb 内部,用户输入的代码是以字符串形式交给 exec 执行,exec 无法感知当前调用栈的外层函数语义,使得匿名函数退回到使用 LOAD_GLOBAL 往全局变量中查找,最终引发报错。 PEP 709 的局限与欺骗编译器的修补思路 (5:00) 虽然 Python 3.12 引入了 PEP 709 内联展开推导式,表面上规避了列表推导式的报错,但根本问题并未解决:任何会产生新 Code Object 的语法(如 lambda 表达式 (lambda: n)())通过 exec 运行时依然会因回退到全局查找而崩溃。由于无法直接修改 exec 底层的变量查找机制,码农高天提出的解决方案是“欺骗编译器”:人为构造一个包含虚拟外层变量(如 n = 0)的 outer/inner 函数结构,让编译器误以为用户代码本来就处于闭包之中,从而生成带有 LOAD_DEREF 引用机制的内层 Code Object。外层定义的虚拟变量仅用于骗过编译期的符号分析,生成 Code Object 后即可丢弃,运行时再由调试器把断点帧中真实的局部变量注入闭包单元。 exec_in_closure 的工程落地与交互闭环 (7:20) 该黑魔法最终在 CPython 的 pdb 中落地为核心函数 exec_in_closure。该函数首先通过 compile 检查常量子元组 co_consts 中是否存在 code 类型对象,若不存在则说明是普通扁平代码,走原生流程执行;若存在,则区分输入是表达式还是语句,利用专用变量 _pdb_eval_result 捕获返回值并决定是否输出。为避免意外污染调用栈,函数在隔离副本中运行,并在执行后记录变更、显式写回原帧的局部变量字典。最后,函数将断点当前帧的局部变量动态组装成闭包元组(Closure Tuple),绑定到伪造代码提取出的 Code Object 上运行,顺利完成变量闭包访问与结果返回。 该核心实现仅有 40 余行有效代码,已从 Python 3.13 开始正式随 CPython 发布,彻底解决了此前被提交过至少 5 次 issue 却因实现难度过高而搁置十余年的调试器变量查找缺陷。

Original transcript

[0:00] 今天给大家带来一段我给C派档写的黑魔法啊理论上说呢我们写程序应该尽可能的避免黑魔法但是如果在cpython里有一个地方最适合黑魔法出现 那就是debugger了在debugger里有时候我们不用黑魔法就没有办法完成我们的需求今天带来的就是PDB里我最喜欢的一段黑魔法的应用我们首先看一下左边的代码非常简单在函数F里我们定义了一个局部变量N等于2接下来这个东西叫做list comprehension对吧这个大家应该看得懂就是从0到9里啊如果这个数能被2整除就把它放进list里最后把这个list打出来这道程序很普通啊甚至可以说很常见打出来就是02468嘛这种List Comprehension是我们非常常用的一种手段然而如果我们在这段函数里面设一个断点调出pdb我们知道在pdb里面是可以在当前位置执行任何代码的我们把这行print原封不动的抄上去 然后运行你会发现 PDB给你报错了他说我找不到这个 N可是如果你尝试打印N 你会发现他知道N是多少有关这个bug,光issue就至少被提出来过5个而这个问题在pdb里至少也存在了十几年了所有人都一直知道这个问题的存在但是大家普遍认为这个问题太难修了我们就先来看一下 这到底是一个什么问题我们先把我们这个F函数给diss一下可以看到上面这个部分是F的本体而在3.11版本之前,List Comprehension会建立一个匿名函数,然后调用这个函数。而这个函数的code object是下面这个部分这里最重要最相关的是这个index14的bycode这个by code读取了N的值 而我们注意一下这里用的by code是loaddrefloadDerift的意思是他知道这个N是通过B包传递的换句话说 当我们运行这个list comprehension的时候编译器知道这个N是从上面的局部变量传下来的所以它会自动完成一个臂包大家可以把它想象成这个状态它实际上是新定义了一个函数在这个函数里,他去拼这个list这个函数里面会用到这个n但是他知道这个N是通过B包传进来的好 为什么 PDB在运行 会出现问题呢因为pdb的处理方式相当于在这里运行了一个exec,也就是把输入的这个string在这里跑一遍我们可以看到如果使用exec的话我们会得到同样的name error因为在这个位置他根本不知道这个N是来源于B包我们可以把这个string在这块this一下前面这个部分也就是print的本体是差不多的但是我们注意这里这个新建的匿名函数里面在尝试读取N的时候使用的是load global他只能通过给进来的这个string判断局部变量里没有N那如果局部变量里没有N的话肯定就是往全局变量里涨了他不会意识到我是在一个函数里运行的 而这个函数的局部变量里 可能有一些变量和我需要的东西形成的比方这就导致了他在找N的时候不是通过B包的形式去找包含他的这个函数里的局部变量而是去找全球变量了这样他必然就找不着这个N了于是这段程序就报错了在3.11和之前的版本里面这个问题一直存在因为他很难解决但是如果你用3.12的话你会发现这个东西突然好使了包括在 PDB 里 它也能用了然而这并不是因为这个问题被解决了而是 least comprehension 被解决了由于3.12版本引入了PEP 709List comprehension这件事已经被inline化了他并不再建立一个新的code object了所以他这里直接用的load name他是可以在给进的locals里面找这个N的但是这只解决了几个相对常见的问题 比如说list和dict的comprehension我们依然有其他的代码有可能会产生新的code object比如说左边这边我们用了lam的函数我们知道lamb的函数是一定会产生新的code object这个LAM函数我们直接调用没有任何问题但是如果我们尝试用EXEC运行它这里就会报同样的错误对吧 name error 这个 n 没有因为它的根源问题并没有被解决那我们显然是没有办法改变exec的这件事不现实但是我们有没有可能让它在pdb里面成功的运行呢当然是可能的对吧因为现在的pdb已经可以运行这种代码了可是在这背后我们付出了多少代价呢接下来正片开始我先来简单介绍一下解决这个问题的思路我们是没有办法改 [5:00] 而我们必须要通过币包来传递变量才能让这一段代码正常运行因为这一段代码它自己建立了一个匿名函数而建立的那个匿名函数是我们没有办法控制的我们没有办法给那段匿名函数建立一个局部变量的namespace所以我们的解法就是欺骗编译器我们要让编译器以为这一段本身就需要币包然后建立一个用币包的code object最后去执行那个code object而不是我们给的这个语句本身大体上说我们要完成这么一件事我们要通过这个给进来的string去构建这么一段代码把这个给进来需要执行的字符串本身放到一个函数里然后强行为这个函数产生臂包现在这个inner函数就是有臂包的这个n就是一个臂包变量相当于我们骗了编译器一手通过让它编译这一串大代码强行产生一个有臂包变量传递的code object而这个code object会最终运行我们给进去那个streamlambda 冒号n然后我们只要里面这个code object外层的那个部分完全是我们为了欺骗编译器而建立的东西我们拿到里面这个code object然后再把臂包变量传进去我们就可以正常的运行这段函数了当然刚才那段代码没有打印任何东西的吧你也不知道它执行了我们把它打印一下同时注意这里我特意把outor里面的n变成了0就是要告诉大家这个n等于0和我们实际执行的函数没有关系我们需要它只是为了得到我们想要的code object我们可以看到这一段代码的运行结果是打印了一个2这个是我们大概的思路接下来我们去看一下在pdb里它究竟是如何实现的这一部分代码的实现都在这个exec in closure里我们给进来的是一个source也就是我们运行的代码是一个string形式的globals和locals分别对应了他们全局和局部变量的明明空间首先我们把这段代码给compile一下然后看看compile这段代码的结果有没有产生任何新的code object如果这段代码并没有产生code object那它就是一个可以直接在当前局部跟全局变量里面执行的代码我们就不用对它做任何特殊的处理当然黑魔法在这个部分就已经开始了大家可能有人看这段代码已经感到费劲了我们是在这个compile好的code object里面去检查它的cost看这个cost里面有没有任何的code type从而判断它有没有产生新的code object接下来把局部变量给复制一份因为我们有可能会不小心修改原来的局部变量而且我们要引入一些临时变量保存一些东西这里是检查输入的这个string是不是一个expression什么叫expression呢比如你直接打一个x加1这就是一个expression你在打x加1的时候你期待的其实是printx加1而如果你打的是y等于x加1这个东西叫一个statement这种statement你是不希望打印出任何东西的如果它是一个expression我们就在这个expression前面加一点东西把它变成一个statement最后根据这个pdbEvalResult里面是不是num来决定要不要打印接下来这个步骤叫做writeback当你输入了x等于1你是希望当前的局部变量x变成1的而由于我们刚才复制了局部变量的命名空间我们在这里运行的结果改变的局部变量并不会影响你打断点函数里面的局部变量所以我们要做一个显示的写回接下来这一段代码不好懂大家主要看注视这个部分其实就是我们右边那个outer函数我们再建立一个新的要compiledStream去欺骗编译器同时我们要的是里面那个函数的code object再下来我们先跑一段这个source跑一段这个source结果是什么相当于运行了一下右边这个outr函数的definition大家注意一下我们没有实际运行这个out函数我们只在这个namespace而定义了这个out函数所以这里我们从namespace里面拿到这个out函数再执行这个out函数注意后面有个小括号这时候的这个code变量就是里面我们构造的那个inner函数的code object了也就是我们拥有了这个支持臂包变量的code object下面这个部分呢是建立这个臂包变量的tuple好把这些变量给传进去我们在这个时候认为每一个当前的局部变量都是币包变量我都往里传而真正运行用户输入的代码是在这个部分我们通过构造拿到了一个用户这段代码在一个币包环境下运行的code object然后运行这个code object [10:00] 这样用户代码就能拿到那些本来只能通过币包机制拿到的变量值了最后把所有需要写回的局部变量写回然后如果需要打印的话总成打印至此这个黑魔法彻底完成这个函数刨去注释与控行一共只有40多行有效代码但是在这40多行有效代码里面充满了跟Python有关的黑魔法如果你能独立看懂这个函数那就说明你的python水准真是到了一定的高度啊看不明白其实也没有关系这期视频其实多少算一个show off其实对我自己解决方案特别满意觉得非常的骄傲因为这个方案确实比较优雅的解决了一个十几年来大家都认为太难的问题那有了这条代码从3.13版本开始大家就可以在pdb里运行很多之前没有办法运行的代码了如果以后你遇到一个人跟你说啊排葬这个语言太简单了谁都能写你就可以把这段代码发给他然后问问他你知道这段代码是什么意思吗好那这一边是李熊拉击点关注别忘了一见再来哦字幕志愿者 杨茜茜