Python借Cell对象绑定函数属性 突破栈帧销毁限制

【python】闭包的实现机制。嵌套函数怎么共享变量的? — 码农高天

码农高天

闭包通过编译期生成的Cell盒子包装共享变量,并将其持久化存入内层函数对象的closure属性中,使变量脱离外层函数调用栈后仍能常驻堆内存。

AI-assisted summary of 【python】闭包的实现机制。嵌套函数怎么共享变量的? — original source. Check the original for full context.

Source published:

Key ideas

  • 闭包绝非在外层函数定义时对局部变量进行静态快照或复制,重新赋值依然能被内层函数实时捕获,这推翻了初学者常见的状态复制假设。 (0:00)
  • Python 编译器在编译期就会静态分析变量作用域,将跨作用域共享的变量标记并生成 MAKE_CELL、STORE_DEREF 等专用字节码指令,而非普通的局部变量指令。 (3:00)
  • 共享变量的底层依赖 cell object(盒子机制)实现间接引用,所有嵌套作用域读写的均是该盒子内部维护的对象指针。 (3:00)
  • 外层函数栈帧销毁后变量不被回收的核心原因,在于 SET_FUNCTION_ATTRIBUTE 指令将包含 cell 的元组直接持久化绑定至内层函数对象的 __closure__(func_closure)属性上。 (6:00)

Detailed notes

## Background 该视频由技术博主“码农高天”于2024年9月6日发布在YouTube平台上。视频简介指出,本期内容聚焦于Python中闭包(closure)的底层实现机制,深入剖析嵌套函数如何能够直接访问并共享外部函数的变量。

## TL;DR 视频首先通过示例反驳了“闭包仅是复制局部变量”的直觉误区,抛出嵌套函数为何能实时同步外部变量重新赋值以及外层栈帧销毁后变量如何存活两大核心疑问。随后,高天反编译字节码,揭示了编译期识别的MAKE_CELL、STORE_DEREF和LOAD_DEREF指令,并以形象的“盒子”比喻解释了cell object作为间接引用的中介机制。最后,视频指出内层函数对象通过SET_FUNCTION_ATTRIBUTE将闭包元组保存在__closure__属性中以维持引用计数,从本质上将闭包抽象为一个打破常规栈帧生命周期的“局部化小全局”空间。

## Atmosphere & style 整期视频洋溢着极具极客范儿的硬核技术探索氛围,节奏明快紧凑且直奔底层本质。高天凭借其资深的CPython源码功底,以从容自信且条理清晰的步调抽丝剥茧,祭出反编译字节码这一“传统艺能”层层揭秘。生动的“盒子”比喻化繁为简,让枯燥的虚拟机指令与内存模型变得具象可感,观看过程充满推导破案般的逻辑快感与顿悟感。

## Bulletin - [00:00](https://www.youtube.com/watch?v=Flce9y5Qn38&t=0s) 闭包绝非在外层函数定义时对局部变量进行静态快照或复制,重新赋值依然能被内层函数实时捕获,这推翻了初学者常见的状态复制假设。 - [03:00](https://www.youtube.com/watch?v=Flce9y5Qn38&t=180s) Python 编译器在编译期就会静态分析变量作用域,将跨作用域共享的变量标记并生成 MAKE_CELL、STORE_DEREF 等专用字节码指令,而非普通的局部变量指令。 - [03:00](https://www.youtube.com/watch?v=Flce9y5Qn38&t=180s) 共享变量的底层依赖 cell object(盒子机制)实现间接引用,所有嵌套作用域读写的均是该盒子内部维护的对象指针。 - [06:00](https://www.youtube.com/watch?v=Flce9y5Qn38&t=360s) 外层函数栈帧销毁后变量不被回收的核心原因,在于 SET_FUNCTION_ATTRIBUTE 指令将包含 cell 的元组直接持久化绑定至内层函数对象的 __closure__(func_closure)属性上。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=Flce9y5Qn38&t=0s) 闭包(Closure)允许内部嵌套函数跨作用域访问并操作外部函数的变量,实现跨调用周期的状态共享。 - [00:00](https://www.youtube.com/watch?v=Flce9y5Qn38&t=0s) 即使在嵌套函数定义完成后再对外部变量重新赋值,内部函数仍能读取到最新值,证明闭包不是简单的局部变量值复制。 - [00:00](https://www.youtube.com/watch?v=Flce9y5Qn38&t=0s) 闭包引发了两个核心底层疑问:变量重新赋值时的状态实时同步机制,以及外部栈帧销毁后变量内存为何能继续驻留。 - [03:00](https://www.youtube.com/watch?v=Flce9y5Qn38&t=180s) Python 在编译期进行静态分析,决定一个变量是生成普通局部变量指令(如 STORE_FAST),还是闭包指令(如 STORE_DEREF)。 - [03:00](https://www.youtube.com/watch?v=Flce9y5Qn38&t=180s) 字节码 MAKE_CELL 负责在底层创建一个 cell object 包装对象,充当容纳变量指针的“盒子”。 - [03:00](https://www.youtube.com/watch?v=Flce9y5Qn38&t=180s) STORE_DEREF 与 LOAD_DEREF 分别通过向 cell 盒子写入与从中解引用,确保所有相关作用域操作的都是同一个内存引用。 - [03:00](https://www.youtube.com/watch?v=Flce9y5Qn38&t=180s) 跨作用域变量在宿主函数中被称为 Cell-Vars,而在引用的嵌套函数中被称为 Free-Vars(自由变量)。 - [06:00](https://www.youtube.com/watch?v=Flce9y5Qn38&t=360s) SET_FUNCTION_ATTRIBUTE 字节码将装有 cell 引用的元组写入返回函数对象的 func_closure 结构中。 - [06:00](https://www.youtube.com/watch?v=Flce9y5Qn38&t=360s) 外部函数的局部栈帧在运行结束后销毁,但函数对象本身常驻堆内存,并通过 __closure__ 属性持续保持对 cell object 的引用计数。 - [06:00](https://www.youtube.com/watch?v=Flce9y5Qn38&t=360s) 检查函数对象的 g.__closure__[0] 可以直接验证其持有的 cell 内部地址与目标数据对象的 id 完全一致。 - [06:00](https://www.youtube.com/watch?v=Flce9y5Qn38&t=360s) 闭包的本质是构建了一个“局部化的全局命名空间”,让外层函数成为其内部嵌套函数的私有“小全局”。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=Flce9y5Qn38&t=0s) 闭包的定义与两个核心疑问

高天开篇直奔主题,探讨 Python 中闭包(Closure)的底层实现机制。他首先通过一段典型的闭包代码进行说明:外层定义一个函数 `f`,其内部初始化了一个变量 `data = []`(空列表);在 `f` 内部又定义了一个嵌套函数 `inner`,它接收一个参数 `value`,内部直接使用了外层的 `data` 变量,将其追加进列表并返回该列表。外层函数 `f` 最终把 `inner` 函数作为对象返回。将 `f()` 的调用结果赋给变量 `g` 后,连续调用 `g(1)` 和 `g(2)`,控制台分别打印出 `[1]` 和 `[1, 2]`。这证明两次调用操作的是同一个列表,并且能够跨越调用周期共享状态。

高天指出,`data` 既不是 `inner` 的常规局部变量(因为 Python 的局部变量以函数为作用域单位),也不是全局变量,“那这种能从一个函数里去读取其他函数变量的机制,我们叫做闭包”。

针对这种现象,有人可能会直觉地认为实现很简单:只要在定义嵌套函数时,把外层函数的局部变量复制一份给内层即可。但高天通过一个反例推翻了这种“简单复制说”:如果在定义完 `inner` 函数之后、在 `f` 返回之前,对 `data` 进行重新赋值(例如赋为一个新的列表,而非原地修改),结果显示 `inner` 打印出来的依然是重新赋值后的新列表。这表明 `inner` 能够持续追踪外层函数中 `data` 变量的变化,绝非简单的定格快照。

由此引出了两个深层次的技术疑问: 1. **状态同步机制**:如果是简单的局部变量复制,为何外层对变量重新赋值后,内层函数能够即时感知并同步? 2. **生命周期谜团**:Python 的局部变量依附于函数运行期的栈帧,函数运行结束后栈帧销毁,局部变量的引用理应失效。既然 `f()` 已经执行完毕返回,为什么本该死去的 `data` 依然能常驻在内存中,供后续多次调用的 `g` 使用?

高天强调,本期使用的是 Python mainline version(前沿开发版源码)。尽管最近几个版本的字节码(bytecode)变动较大,但闭包的核心架构与总体实现方向始终是一致的。

```mermaid flowchart TD accTitle: 闭包外部函数与内部函数的引用关系 accDescr: 展示函数 f 内部创建变量与闭包函数,返回后通过 g 调用的结构流程

subgraph OuterScope ["外层函数 f() 调用期"] A["初始化变量 data = []"] --> B["定义嵌套函数 inner(value)"] B --> C["对 data 重新赋值或变更操作"] C --> D["返回 inner 函数对象"] end

D --> E["赋值给外部变量 g = f()"]

subgraph ExecutionScope ["生命周期结束后的独立调用"] E --> F["调用 g(1) -> 操作同一个 data"] F --> G["调用 g(2) -> 持续追踪最新 data 状态"] end ```

---

[03:00](https://www.youtube.com/watch?v=Flce9y5Qn38&t=180s) 字节码揭秘:Cell 对象与变量共享

带着疑问,高天祭出“传统艺能”——反编译查看函数的字节码(bytecode)。通过对比外层函数 `f` 和内层函数 `inner` 的 Code Object,可以发现一系列平常罕见的特定字节码指令: - 在 `f` 的字节码中,出现了 `MAKE_CELL`、`STORE_DEREF` 以及 `SET_FUNCTION_ATTRIBUTE`(附带 closure 参数); - 在 `inner` 的字节码中,读取参数 `value` 使用的是常规的局部变量加载指令 `LOAD_FAST`,而读取 `data` 时却使用了特殊的 `LOAD_DEREF`。

高天逐一拆解这些指令的真实作用。首先是 `MAKE_CELL`,它的底层核心逻辑极度纯粹,就是创建一个 `cell object`。高天打了一个非常生动形象的比喻:“你可以把它理解为一个盒子,我们把变量装到这个盒子里,而在每一个 scope 里修改变量的时候,实际上修改的是盒子里面保存的那个变量,这样就可以保证每一个 scope 看到的是同一个变量,而不是一个变量被复制了很多次。”

接着是 `STORE_DEREF`。它的运行逻辑与盒子模型完全吻合:取出装着变量的 cell 盒子,把盒子里原有的对象指针扔掉,再将新的变量引用装入盒中。正因如此,无论哪个作用域去打开这个盒子,拿到的都是最新装入的值,完美解释了为什么重新赋值依然能被内层函数追踪。

更关键的是指令生成的决策时机。变量到底编译成普通的 `STORE_FAST` 还是包装 cell 的 `STORE_DEREF`,完全由 Python 编译期(Compile Time)决定。编译器静态分析代码结构,识别出一个变量究竟属于纯粹的单函数局部变量,还是跨越嵌套作用域的闭包共享变量。在编译期生成不同的字节码,从而在运行时产生完全不同的行为。

内层函数 `inner` 在读取 `data` 时所用的 `LOAD_DEREF` 也是对称的逻辑:先从内部上下文拿到这个特定的 cell 盒子,再从盒子里解引用取出实际存储的值。对于同一个 `data` 变量,在宿主函数 `f` 中它被称为 **Cell-Vars**(名称正是取自 `cell object`),而在消费它的内层函数 `inner` 内部则被称为 **Free-Vars**(自由变量,即“非我所定义、从外部捕获的变量”)。在函数实例化时,Python 会调用底层的 `copy_freevars` 逻辑完成这些 cell 引用的传递对接。

---

[06:00](https://www.youtube.com/watch?v=Flce9y5Qn38&t=360s) 闭包的生命周期本质与“小全局”模型

虽然理清了 `cell object` 盒子的运作,但核心的内存生命周期问题仍未解决:Python 依赖引用计数机制管理内存垃圾回收,既然外层函数 `f` 已经执行结束,运行栈被清空,究竟是谁在持有这个 cell 盒子,从而维持 `data` 的生命?

高天指出了答案的关键转折点:字节码指令 `SET_FUNCTION_ATTRIBUTE`。在构造返回的 `inner` 函数对象时,该指令把包含所有共享 cell 的元组(tuple)作为 `closure` 属性写入了新生成的 Function Object 中(对应底层 C 结构体的 `func_closure`)。

高天一语道破天机:“也就是说,这些 cell variable 实际上活到了 function object 里面。” - 当执行 `g = f()` 时,外层创建并返回了一个函数对象 `g`; - 虽然函数对象在未被调用时内部不包含任何局部变量栈帧,但它本身作为一个持久驻留在堆内存的对象,其属性 `__closure__`(即 `func_closure`)牢牢持有着包含 `cell object` 的元组; - `cell object` 内部再持有实际的 `list` 对象(即 `data`)。

通过打印检测可以发现,`g.__closure__[0]` 确实就是一个 `cell`,其内部引用的内存地址与最终返回打印的 `data` 列表的 `id` 完全一致。只要变量 `g` 依然存活,对该函数对象的引用不断,闭包元组及内部的 cell 盒子就绝不会被垃圾回收释放,进而保证了共享数据在堆内存中的永续生存。

最后,高天对闭包的本质做了一个高屋建瓴的抽象总结。他认为闭包在机制上其实并不神秘,实质上就是一种“建立局部化全局命名空间”的手段。如果不看第一行最外层的 `def f():`,将内层代码平铺,`data` 就像是一个顶层全局变量,而 `inner` 只是读取这个全局变量并执行运算:

> “所谓闭包,形式上就是让 f 这个函数成为了一个新的小全局,在函数 f 里可以定义属于这个函数 f 的‘全局变量’,然后所有定义在这个函数里的其他函数都可以去使用这个全局变量。”

闭包最神奇与精妙之处,就在于打破了常规局部栈帧的生死限制:即使宿主函数 `f()` 已经彻底执行结束,甚至外部把对 `f` 自身的引用完全抹去,这些活在内层函数对象 `func_closure` 里的闭包变量依然鲜活地存在于内存之中。

```mermaid flowchart LR accTitle: Python 闭包变量在堆内存中的引用链 accDescr: 展示函数对象 g 通过 func_closure 链式保持引用,从而防止变量被垃圾回收释放

subgraph FunctionObj ["Function Object: g"] A["函数对象 g"] -->|"持有属性"| B["__closure__ 元组 (Tuple)"] end

subgraph CellWrapper ["Cell 封装层"] B -->|"元组元素"| C["Cell Object (盒子)"] end

subgraph HeapData ["实际数据对象"] C -->|"内部指针 ob_ref"| D["List Object: data [1, 2]"] end ```

- **核心概念对比**: - **Cell-Vars**:在外层函数 `f` 中定义、需要逃逸出自身栈帧供内层使用的变量。 - **Free-Vars**:在内层函数 `inner` 中直接使用、但未在当前函数局部作用域内声明的自由变量。 - **关键底层字节码**: - `MAKE_CELL`:在当前作用域为共享变量创建 `cell object` 包装盒。 - `STORE_DEREF`:将对象引用装入特定的 cell 盒子中。 - `LOAD_DEREF`:通过 cell 盒子间接解引用获取数据对象。 - `SET_FUNCTION_ATTRIBUTE`:将装有 cell 引用的元组持久化绑定到内层函数对象的闭包属性上。

## Notable quotes > [06:00](https://www.youtube.com/watch?v=Flce9y5Qn38&t=360s) 所谓闭包,形式上就是让 f 这个函数成为了一个新的小全局,在函数 f 里可以定义属于这个函数 f 的‘全局变量’,然后所有定义在这个函数里的其他函数都可以去使用这个全局变量。 — 高天

## Mentioned 高天, Python, YouTube

## Video description 这期视频我们讲一下闭包,也就是closure的实现机制。我们在一个函数里定义另一个函数的时候,会发现内部的函数可以直接使用外部函数的变量,那这个机制的内部实现是什么样的呢?

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

中文简报

Python借Cell对象绑定函数属性 突破栈帧销毁限制 闭包通过编译期生成的Cell盒子包装共享变量,并将其持久化存入内层函数对象的closure属性中,使变量脱离外层函数调用栈后仍能常驻堆内存。 视频首先通过示例反驳了“闭包仅是复制局部变量”的直觉误区,抛出嵌套函数为何能实时同步外部变量重新赋值以及外层栈帧销毁后变量如何存活两大核心疑问。随后,高天反编译字节码,揭示了编译期识别的MAKE_CELL、STORE_DEREF和LOAD_DEREF指令,并以形象的“盒子”比喻解释了cell object作为间接引用的中介机制。最后,视频指出内层函数对象通过SET_FUNCTION_ATTRIBUTE将闭包元组保存在__closure__属性中以维持引用计数,从本质上将闭包抽象为一个打破常规栈帧生命周期的“局部化小全局”空间。 闭包绝非在外层函数定义时对局部变量进行静态快照或复制,重新赋值依然能被内层函数实时捕获,这推翻了初学者常见的状态复制假设。 (0:00) Python 编译器在编译期就会静态分析变量作用域,将跨作用域共享的变量标记并生成 MAKE_CELL、STORE_DEREF 等专用字节码指令,而非普通的局部变量指令。 (3:00) 共享变量的底层依赖 cell object(盒子机制)实现间接引用,所有嵌套作用域读写的均是该盒子内部维护的对象指针。 (3:00) 外层函数栈帧销毁后变量不被回收的核心原因,在于 SET_FUNCTION_ATTRIBUTE 指令将包含 cell 的元组直接持久化绑定至内层函数对象的 __closure__(func_closure)属性上。 (6:00)

Reading summary

码农高天指出,Python 闭包并非单纯对外部变量进行定格快照或复制,而是通过编译期静态分析识别自由变量,利用 Cell 对象作为间接容器,并将该容器直接绑定到内层函数对象本身以突破栈帧生命周期的限制。 状态同步悖论与直觉复制说的破除 (0:00) 在典型闭包场景中,外层函数创建变量与内层嵌套函数并将其返回,后续多次调用该返回函数会共享同一变量状态。高天通过反例推翻了“定义内层函数时只是将外层局部变量复制一份”的直觉猜想:若在内层函数定义完成之后、外层函数返回之前对外层变量重新赋值,内层函数依然能精确反映重新赋值后的新状态,证明其能持续动态追踪变量变化。 这引发了两个核心底层疑问:一是外层变量发生重新赋值时,内层函数依靠何种机制保持即时同步;二是 Python 局部变量本应随函数执行完毕及调用栈帧销毁而释放,为何本该随宿主函数一同消亡的数据却能跨越调用周期常驻于内存之中供后续调用。 Cell对象封装与编译期指令决策 (3:00) 通过反编译 Python 字节码,高天解释了变量动态同步的实质机制。编译器在编译期静态分析代码结构,判断变量属于普通局部变量还是跨嵌套作用域共享的变量。对外层定义的共享变量(Cell-Vars),生成 `MAKE_CELL` 指令创建 `cell object`,而读写则采用 `STORE_DEREF` 与 `LOAD_DEREF` 指令,而非普通的 `STORE_FAST` 或 `LOAD_FAST`。 高天将 `cell object` 比喻为一个充当指针中介的“盒子”。在不同作用域内修改或重新赋值时,执行的实际上是 `STORE_DEREF`,即替换盒子内部存储的对象指针。内层函数使用这些非自身定义的自由变量(Free-Vars)时,通过 `LOAD_DEREF` 解引用取出盒子内的实际数据,从而保证不同作用域始终指向同一个容器及其内部最新状态。 函数对象承载与小全局模型 (6:00) 针对栈帧销毁后的生命周期问题,关键在于字节码指令 `SET_FUNCTION_ATTRIBUTE`。在内层函数实例化时,底层会通过 `copy_freevars` 逻辑传递引用,将包含所有共享 Cell 盒子的元组持久化写入新生成函数对象的 `closure` 属性(对应底层 C 结构体的 `func_closure`,即 Python 中的 `__closure__`)。只要返回的内层函数对象本身在外部持有引用,Cell 对象及其内部引用的数据对象便不会被垃圾回收释放。 高天据此将闭包的本质概括为一种“局部化的全局命名空间”模型。闭包形式上使得外层宿主函数成为一个专属的“小全局”,内部定义的变量如同该范围内的专属全局变量供所有内层函数使用;其核心机制在于冲破了传统调用栈帧随调用结束即销毁的生命期约束,使变量依附于内层函数对象在堆内存中持续存活。 高天说明,该实现解析基于 Python 前沿开发版源码,尽管近几个版本的字节码指令有所变动,但闭包依托 Cell 对象封装以及将引用寄宿在函数对象属性中的核心架构始终保持一致。

Source quotes

[6:00] 高天: 所谓闭包,形式上就是让 f 这个函数成为了一个新的小全局,在函数 f 里可以定义属于这个函数 f 的‘全局变量’,然后所有定义在这个函数里的其他函数都可以去使用这个全局变量。

Original transcript

[0:00] 今天这期视频我们来聊一下拍档中必包的实现机制首先我们来简单说一下什么叫臂包我们看左边这个代码我们定义了一个函数 FF里面有一个变量Data是一个空List那在F里面我们又定义了另外一个函数叫做inner这个inner函数里面使用了data这个变量那很明显Data这个变量它不属于局部变量因为python的局部变量是以函数为单位的对吧这个变量并不是只在inner函数里面存在同时它又不是全局变量 非常明显对吧那这种能从一个函数里去读取其他函数变量的机制啊我们叫做必报我们可以看到这个F函数运行的结果是返回这个inner函数我们把这个结果保存给G这个时候G跟inner都是一样的 同一个函数这个in函数有一个参数value他每次会把这个value加到data这个list里面然后把data这个list返回我们简单跑一下啊打印一下G1再打印一下G2可以看到G1把1加到这个list里面并且打印出来G2把2又加到这个例子里面打印出来的是1 2也就是说,这两次G的调用力,或者说是inner的调用力他们使用的data是同一个data是s里面我们初始化的那个data那这种机制是怎么实现的呢有人说很简单啊对吧我只要在函数定义的时候把外部函数的所有局物变量复制一份复制到里面这个函数不就可以了吗那如果真的是在定义函数的时候复制局部变量这么简单你就没法解释下面这件事我们看我们在定义完inner这个函数之后 我们把data再重新进行一次复制注意我们做的是对变量的重新复制而不是修改变量如果臂包的实现机制是简单的复制局部变量的话我们这一次对data的重复值不应该影响到我们inner里面的那个data当然了从结果上大家可以看到inner里面那个data使用的是被重新复制之后的这个data也就是说他好像知道在F里面这个data发生了什么它可以一直追踪这个函数F里面的data变量不仅如此,我们想一下如果是简单的局部变量复制那data这个值,它被保存在哪了呢?我们知道一个函数是不会引用它的局部变量的也就是说一个函数只有在运行的时候才会重新去建立这个局部变量在运行完之后这个局部变量就被扔掉了那data是f的局部变量对吧在我们调用f之后,f这个函数对data的引用就应该失效了那凭什么我们在反复调用G的时候这个data还一直被保存在内存里呢今天我们就带着大家来研究一下这个问题首先要说明的是今天我们使用的python版本是 Python 的 mainline version也就是最前沿的python版本大家知道呢在最近这几个版本中python的bycode都有比较大的变化所以不同的版本之间可能有些差别但是总体的实现方向是一致的首先是我们的传统异能打开bycode大家可以看到在f函数的codeobject那里出现了一些我们平时不熟悉的bypcode比如说最开始的这个 make cell包括后面的StoreDraft这里还有 set function attribute closure而在这个inner函数的code object里面他在读取data的时候也不像读取value一样使用loadfast而是使用了loadDref那这些都和我们的臂包机制息息相关我们一个一个来解读啊从make sell开始我们看make cell的代表非常简单啊 它的核心只有一件事 就是建立了一个cell object而这个cell object就是专门被拿来保存被多个scope所引用的变量的你可以把它理解为一个盒子我们把变量装到这个盒子里而在每一个scope里修改变量的时候实际上修改的是盒子里面保存的那个变量这样就可以保证每一个scope看到的是同一个变量而不是一个变量被复制了很多次那当然这里的代码啊,在make sell之后,核心就是这个store derive我们看这个代码就和我们刚才说的一样拿到这个装着变量的盒子把现在盒子里面装着东西拿出来扔了把这个新的值新的变量装进盒子里这样在其他的scope再去尝试拿盒子里东西的时候他们就都能知道盒子里东西变了而值得注意的是,这里的bycode是用store derive,还是store fast,也就是针对 [5:00] 逾步变样的方式是Python的编译器决定的也就是说在python代码的编译期Python会识别每一个变量到底是单纯的局部变量还是需要币包时候用的变量在编译期被识别产生了不同的by code 于是出现了不同的run time behavior那可想而知在inner这个函数里在读取data的时候使用的这个loadderif肯定也采用了相似的机制大家可以看到啊就是从 local variable 里面拿到这个 cell再从这个cell里拿到实际的值而这些cell是在copy freevarts里面被复制过来的当python编译器发现一个函数使用了其他函数的变量就会跑一下copy freevars同样的变量data在inner这个函数里面它就是一个freevalfree代表不是我这个函数定义的变量而在函数F里,它一般被称作Cell-Vars这个cell就取自这个cell object那我们刚才还有一个问题没有解决对吧就是这些变量它保存在哪了我们知道 Python 是一个引用技术的机制你这个data不管你是sellers还是freeers一定得有一个东西对你保持引用你才能活着那我们知道data先被放到了一个cell里,所以data本身肯定有cell对它的引用那问题就是谁在引用cell或者说cell活在哪了这时候就让我们的目光转向这个set function attribute这里有一个大大的closure也就是臂包的英文我们可以看到在set function attribute这个bytecode里面他把这个ATTR复制给了这个FunctionObject下面的FunctionClosures而这个 attr 就是我们这里建立的这个tuple这个tuple里面包含了我们所有需要传递的sell variable也就是说这些cell variable实际上活到了function object里面也就是说当我们做G=F的时候我们建立了一个function object g这个FunctionObject里面虽然没有保存任何的局部变量就像我刚才说的 你只有调用这个function object才会产生局部变量在这个function object运行结束的时候这些局部变量就没了但是这个FunctionObject本身保存了臂包变量事实上我们可以找到这个FunctionObjectMail保存的FreeVars他就在这个closure attribute里我们可以看到closure里保存了一个cell object而这个cell object里面存放的是一个list object也就是我们的data因此只要G还活着就永远有对Data的引用我们每一次这个append也永远可以放到同一个data里我们可以看到closure里面保存的这个list object和返回回来这个data list他们俩的ID是一致的那臂包这个事情他本身的没什么神秘的从结果上看他只是建立了一个新的scope如果我们不看第一行这个DEF其他的代码就瞬间变得好理解了很多data 就是一个全局变量嘛那inner就是一个函数使用了一个全区变量的那如果这个全局变量在定义完inner之后改变了那inner在access这个全局变量的时候当然也会拿到不同的值这非常容易理解对吧所谓B包形式上就是让F这个函数成为了一个新的小全局在函数F里可以定义属于这个函数F的群聚变量然后所有定义在这个函数里的其他函数都可以去使用这个全局变量当然它比较神奇的地方在于即使F的调用结束了甚至对F的引用都没有了这个臂包变量啊它依然能活着好的这就是这边的诺别,喜欢和小姐的分享关注,别忘了可以接三连哦!字幕志愿者 杨茜茜