CPython移除方法对象Free List 终结内存ID复用假象

【python】关于method的玄学?这id怎么这么难以预测?让我们用科学解释玄学! — 码农高天

码农高天

方法获取时的ID一致本质上是内存快速释放与重用的巧合,而Python 3.6优化直接调用路径后,官方在3.9版本中果断剔除了已沦为冗余负担的free_list缓存机制。

AI-assisted summary of 【python】关于method的玄学?这id怎么这么难以预测?让我们用科学解释玄学! — original source. Check the original for full context.

Source published:

Key ideas

  • 在 Python 中连续获取同一对象的方法并计算 ID,其结果在 Python 3.9 下未加 print 时相同,但插入一行 print 后就会变成两个不同的地址;而在 Python 3.8 下则始终保持一致。 (0:00)
  • 每次通过属性访问获取方法时,底层的 descriptor 实际上都会新建一个独立的 PyMethodObject,初始测试 ID 相等本质上是生命周期紧密交替导致的内存复用巧合。 (2:50)
  • Python 3.8 针对 method 对象保留了 free_list 内存缓存机制,因而即使插入复杂的 print 扰乱内存,新建方法依然能命中缓存;而 Python 3.9 彻底移除了该机制。 (2:50)
  • 移除 free_list 的深层原因在于 Python 3.6 引入了 LOAD_METHOD 与 CALL_METHOD 字节码优化,直接调用方法无需创建临时对象,导致创建 PyMethodObject 的场景极少,保留缓存反成累赘。 (5:00)

Detailed notes

## Background 该视频由 YouTube 频道“码农高天”于 2024 年 2 月 3 日发布。视频灵感源自 B 站粉丝提出的疑问,作者通过精炼后的测试代码,旨在用科学的底层原理剖析 Python 中看似离奇的“玄学”现象,并传达面对玄学不信谣、不传谣的技术态度。

## TL;DR 视频探讨了连续获取对象方法并查看 `id(o.f)` 时,为何在 Python 3.9 中插入一行 `print` 就会导致前后 ID 从一致变为不一致,而在 Python 3.8 中却始终保持相同的现象。高天解释道,方法属性本质上是通过 descriptor 每次动态生成独立的 `PyMethodObject`,初次测试 ID 相同仅是内存释放后恰好复用的巧合,而 `print` 打乱了内存状态;Python 3.8 始终一致则归功于底层针对 method 的 `free_list` 缓存机制。进一步深挖 CPython 源码演进可知,由于 Python 3.6 引入了 `LOAD_METHOD` 字节码优化使得直接调用方法不再需要构建临时对象,`free_list` 功效减弱并成为累赘,最终在 Python 3.9 中被官方彻底移除。

## Atmosphere & style 整体氛围严谨求真且风趣幽默,开篇以开发者在日常 Debug 中“加了一行 print 后 bug 离奇消失”的真实痛点切入,瞬间拉近与观众的距离。高天的讲解层层递进、逻辑清晰,既有对“玄学”现象的生动调侃,又有直面底层 C 源码与字节码硬核剖析的理性克制。整期内容充满探索与解密的智趣感,展现了资深开发者回归科学、追根溯源的技术魅力。

## Bulletin - [00:00](https://www.youtube.com/watch?v=2GpCIKkEOuw&t=0s) 在 Python 中连续获取同一对象的方法并计算 ID,其结果在 Python 3.9 下未加 print 时相同,但插入一行 print 后就会变成两个不同的地址;而在 Python 3.8 下则始终保持一致。 - [02:50](https://www.youtube.com/watch?v=2GpCIKkEOuw&t=170s) 每次通过属性访问获取方法时,底层的 descriptor 实际上都会新建一个独立的 PyMethodObject,初始测试 ID 相等本质上是生命周期紧密交替导致的内存复用巧合。 - [02:50](https://www.youtube.com/watch?v=2GpCIKkEOuw&t=170s) Python 3.8 针对 method 对象保留了 free_list 内存缓存机制,因而即使插入复杂的 print 扰乱内存,新建方法依然能命中缓存;而 Python 3.9 彻底移除了该机制。 - [05:00](https://www.youtube.com/watch?v=2GpCIKkEOuw&t=300s) 移除 free_list 的深层原因在于 Python 3.6 引入了 LOAD_METHOD 与 CALL_METHOD 字节码优化,直接调用方法无需创建临时对象,导致创建 PyMethodObject 的场景极少,保留缓存反成累赘。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=2GpCIKkEOuw&t=0s) 测试代码在连续获取 a = id(o.f) 与 b = id(o.f) 时,插入一行 print(a) 会破坏 ID 的一致性,呈现典型的调试干扰假象。 - [00:00](https://www.youtube.com/watch?v=2GpCIKkEOuw&t=0s) 该现象在不同版本表现不一:Python 3.9 插入 print 后 a 与 b 永远不等,而 Python 3.8 下无论是否打印两者均永远相等。 - [02:50](https://www.youtube.com/watch?v=2GpCIKkEOuw&t=170s) 从底层对象模型来看,访问 o.f 会通过 function descriptor 创建独立的 PyMethodObject,理论上每次生成的对象地址天然不同。 - [02:50](https://www.youtube.com/watch?v=2GpCIKkEOuw&t=170s) 未加 print 时两处 ID 相同完全是巧合:前一个临时对象的内存刚被释放,紧接着创建的后一个对象恰好复用了同一块地址。 - [02:50](https://www.youtube.com/watch?v=2GpCIKkEOuw&t=170s) print 函数内部具有复杂的执行逻辑与多次内存分配回收,插入 print 会打乱底层通用内存分配状态,导致第二个方法对象无法复用原地址。 - [02:50](https://www.youtube.com/watch?v=2GpCIKkEOuw&t=170s) 若显式通过变量引用持有 o.f 阻止其内存被立即释放,再次获取方法对象时 ID 同样会出现不相等。 - [02:50](https://www.youtube.com/watch?v=2GpCIKkEOuw&t=170s) Python 3.8 的 PyMethod_New 实现了 free_list 机制,方法销毁时不向操作系统归还内存而是暂存列表,确保下次创建稳定复用。 - [02:50](https://www.youtube.com/watch?v=2GpCIKkEOuw&t=170s) Python 3.9 核心源码删除了针对 method 对象的 free_list 缓存逻辑,导致其地址复用极易受到外部内存分配干扰。 - [05:00](https://www.youtube.com/watch?v=2GpCIKkEOuw&t=300s) 传统属性访问方式 a = o.f 生成 LOAD_ATTR 字节码,必须完整构建出 PyMethodObject 实体并完成变量赋值。 - [05:00](https://www.youtube.com/watch?v=2GpCIKkEOuw&t=300s) 自 Python 3.6 起,直接调用 o.f() 被优化为 LOAD_METHOD 与 CALL_METHOD,底层借助 _PyObject_GetMethod 快速查找函数与 self 指针入栈。 - [05:00](https://www.youtube.com/watch?v=2GpCIKkEOuw&t=300s) LOAD_METHOD 的优化使得绝大部分常规方法调用完全跳过了实例化临时 PyMethodObject 的步骤,大幅降低了创建开销。 - [05:00](https://www.youtube.com/watch?v=2GpCIKkEOuw&t=300s) 实际生产环境中单独取出方法赋值的场景极罕见,当优化代码(free_list)带来的收益微乎其微时,维护它反而成了系统累赘,因此 Python 3.9 最终将其清理。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=2GpCIKkEOuw&t=0s) 一个令人困惑的玄学现象:相同的代码为何 ID 一会儿一样、一会儿不一样?

```mermaid flowchart TD accTitle: Python Method ID 玄学对比流程图 accDescr: 展示在 Python 3.8 与 3.9 环境下插入 print 代码前后获取 method ID 的行为差异

subgraph Case1["原始代码 (Python 3.9)"] A1["a = id(o.f)"] --> B1["b = id(o.f)"] B1 --> C1["print(a, b) -> a == b (巧合:内存复用)"] end

subgraph Case2["插入 print(a) (Python 3.9)"] A2["a = id(o.f)"] --> P2["print(a) 打乱内存分配"] P2 --> B2["b = id(o.f)"] B2 --> C2["print(a, b) -> a!= b (地址改变)"] end

subgraph Case3["Python 3.8 环境"] A3["a = id(o.f)"] --> P3["print(a) 或其他复杂操作"] P3 --> B3["b = id(o.f)"] B3 --> C3["print(a, b) -> a == b (free list 保留内存)"] end ```

在日常的 Python 编程与调试过程中,开发者往往会遇到一些难以理解的怪异现象。码农高天分享了一个由 B 站粉丝提出的问题,并将其提炼为一段极简的测试代码。代码首先定义了一个包含方法 `f` 的类 `C`,接着实例化出对象 `o`。随后分别令 `a = id(o.f)` 以及 `b = id(o.f)`。面对“`a` 与 `b` 是否相等”这一问题,许多人的第一反应可能是脑中一片空白,而看过此前关于方法底层原理视频的观众则可能会意识到这里暗藏玄机。运行这段基础代码后,结果显示 `a` 和 `b` 是一致的。

然而“玄学”的调试陷阱随即展现:如果在计算完 `a = id(o.f)` 之后立即加入一行 `print(a)`,其余代码保持原封不动,再次运行程序,打印出的 `a` 与 `b` 竟然变成了不同的两个值。码农高天感叹道:“我不知道大家在 debug 的时候有没有过这种经历,这个 bug 我加了一个 print 之后,它就没法复现了,我们今天就来表演一下这个场面。”这种原本一致的 ID 仅仅因为加了一行看似无关的打印语句而变得不一致,很容易让程序员归咎于所谓的玄学。

更离奇的是跨版本的行为差异。在当前使用的 Python 3.9 虚拟环境中,插入 `print(a)` 后 `a` 与 `b` 永远不一致;但如果切换至 Python 3.8 环境,无论运行多少次,`a` 与 `b` 却又永远保持一致。码农高天指出,面对这种“有时候一致、有时候不一致”的现象,视频的目的绝不是停留于表面玄学,而是要回归科学,深入分析解释底层机制到底发生了什么。

- 基础复现代码逻辑: 1. `class C: def f(self): pass` 2. `o = C()` 3. `a = id(o.f)` 4. 插入 `print(a)` 扰乱测试环境 5. `b = id(o.f)` 6. 对比 `a` 与 `b` 的值 - 运行现象对比: - Python 3.9 未加 `print`:`a == b` - Python 3.9 加上 `print(a)`:`a!= b` - Python 3.8 无论是否加 `print(a)`:`a == b`

---

[02:50](https://www.youtube.com/watch?v=2GpCIKkEOuw&t=170s) 回归科学解释:内存巧合、Descriptor 与 Free List 机制

要彻底理解这一现象,必须回到 `o.f` 本质上究竟是什么这一核心问题。在 Python 对象的底层模型中,每一次尝试从对象上获取一个方法时,实际上都会通过函数自带的 descriptor 新建一个 `PyMethodObject`。换而言之,每次访问 `o.f` 时生成的都是两个不同的独立对象,其地址理论上天然就应该不一样。如果理解了这一点,就会发现最开始得出“两处 ID 相等”的结论完全是一个巧合。码农高天直言:“但如果你意识到了这件事儿,你就会发现你刚开始那道题答案是错的,因为在当前这个程序下 A 跟 B 就是相等的,那这里怎么解释呢?就是两个字叫巧合。”

这个巧合的根源在于 Python 底层的内存管理机制。当代码连续执行两次获取操作时,由于第一个 `o.f` 和第二个 `o.f` 的生命周期衔接得足够紧密,第一个 `o.f` 在计算完 `id()` 后生命周期结束并立刻释放了内存,紧接着生成的第二个 `o.f` 恰好重用了刚刚释放的这块内存地址,从而导致两者的 `id()` 输出完全相同。然而,一旦在中间插入 `print(a)`,由于 `print` 函数本身内部的执行逻辑极为复杂,涉及多次内存分配与回收,彻底打乱了原有的内存管理状态,第二个 `o.f` 无法再使用原地址,`a` 与 `b` 的差异便暴露了出来。类似地,若通过 `x = o.f` 建立一个引用并保存,阻止了第一块内存被回收,再次获取时也会观察到 `a` 与 `b` 不相等。

针对 Python 3.8 与 3.9 的表现差异,答案则隐藏在 CPython 源码的演化中。在 Python 3.8 的 `PyMethod_New` 函数源码中,存在一个叫做 `free_list`(空闲列表)的内存缓存机制。当一个 `method` 对象的生命周期结束、原本应当释放内存时,CPython 并不立即向操作系统归还该内存,而是将其暂存在 `free_list` 中保留下来;当下一次需要新建方法对象时,就可以直接复用这块内存,无需重新向系统申请。这种缓存机制极具稳定性,即使中间穿插了执行极为繁杂的 `print` 代码打乱通用内存,后续新建方法依然能精准从 `free_list` 中命中该地址,因而在 Python 3.8 下两个 ID 始终一致。而在 Python 3.9 的 CPython 源码中,这段针对 method 的 `free_list` 逻辑已被彻底移除,导致其内存无法被强制保留,版本之间的差异由此产生。

---

[05:00](https://www.youtube.com/watch?v=2GpCIKkEOuw&t=300s) 追根溯源:为什么 Python 3.9 移除了 Method 的 Free List?

既然 `free_list` 听起来能有效避免频繁申请和释放内存的时间开销,官方为何要在 Python 3.9 中将这段性能优化代码直接删除?码农高天表示,解答这种问题正是深入剖析底层的意义所在,因为“正常人绝对不会跟你讲这么深”。要厘清删除的原因,必须回溯到 Python 3.6 对方法调用机制所做的一项重大优化。

在过去的传统认知中,通过 descriptor 机制访问方法每次都要构建一个新的 `PyMethodObject`,如果每次调用对象方法都要凭空新建一个对象然后再销毁,性能开销确实会显得非常笨拙。面对粉丝关于这一机制是否很蠢的疑惑,码农高天坦率评价道:“答案是蠢。这也就是为什么从 Python 3.6 开始这件事情被优化了。”

```mermaid flowchart LR accTitle: LOAD_ATTR 与 LOAD_METHOD 字节码路径对比 accDescr: 比较常规赋值与直接方法调用在字节码及底层 CPython 逻辑上的差异

subgraph Traditional["普通属性获取: a = o.f"] direction TB T1["LOAD_ATTR 字节码"] --> T2["调用 function descriptor"] T2 --> T3["创建完整的 PyMethodObject"] T3 --> T4["赋值保存至变量 a"] end

subgraph Optimized["直接方法调用: o.f() (Python 3.6+)"] direction TB O1["LOAD_METHOD 字节码"] --> O2["_PyObject_GetMethod 快速查找"] O2 --> O3["将 Descriptor 函数直接推入栈"] O3 --> O4["将 self 对象推入栈"] O4 --> O5["CALL_METHOD 直接绑定调用 (无需临时 Object)"] end ```

通过对比字节码可以清晰看到优化前后的差异: - **普通绑定场景(如 `a = o.f`)**:生成的字节码为 `LOAD_ATTR`。底层必须触发该函数的 descriptor 机制,完整创建出一个 `PyMethodObject` 实体并赋给变量 `a`。 - **直接调用场景(如 `o.f()`)**:从 Python 3.6 起,字节码被优化为了专用的 `LOAD_METHOD` 与 `CALL_METHOD` 组合,替代了以往低效的 `LOAD_ATTR` 加 `CALL_FUNCTION`。

在 `LOAD_METHOD` 的底层 C 源码中,CPython 调用 `_PyObject_GetMethod` 进行高速查找。一旦确认该属性是一个方法,底层便直接将函数的 descriptor(即类中原始定义的 `f` 函数指针)与实例对象 `self` 先后压入求值栈,随后在 `CALL_METHOD` 阶段直接调用该函数并传入 `self`,整个过程完全跳过了创建临时 `PyMethodObject` 的步骤。在实际生产代码中,绝大多数操作都是对方法进行即时调用,将方法单独取出来赋值给变量的用法极为罕见。

正是由于 Python 3.6 引入的这一优化,让绝大部分方法调用都不再需要实例化临时对象,导致在 Python 运行时中真正去执行 `PyMethod_New` 创建新方法对象的场景变得微乎其微。码农高天由此阐释了底层架构演进的权衡之道:“而我们知道任何代码都是有代价的,当你一段优化的代码功效变小的时候,这段优化的代码可能就反而成为了累赘。”由于 `free_list` 的管理和维护本身带有维护成本和内存开销,既然新建 method 的场景已经极少发生,继续保留它便失去了价值。最终,Python 3.9 核心团队果断移除了这段过时的 `free_list` 优化代码,而正是这一清理遗留优化的决策,构成了这期“玄学”现象的底层真相。码农高天在结尾呼吁大家面对玄学问题要用科学眼光深究原因,做到不信谣、不传谣,并提醒观众一键三连以支持深度技术内容的持续输出。

## Notable quotes > [05:00](https://www.youtube.com/watch?v=2GpCIKkEOuw&t=300s) 任何代码都是有代价的,当你一段优化的代码功效变小的时候,这段优化的代码可能就反而成为了累赘。 — 码农高天

## Mentioned Python, CPython, Python 3.6, Python 3.8, Python 3.9, 码农高天, B站

## Video description 这期视频的灵感来自于某个B站粉丝发给我的问题,我精简了一下拿了出来。这期视频的背后有很多用处不是很大,但是蛮有趣的知识点,但是更重要的是传达了我们对玄学的态度——不信谣,不传谣。

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

中文简报

CPython移除方法对象Free List 终结内存ID复用假象 方法获取时的ID一致本质上是内存快速释放与重用的巧导,而Python 3.6优化直接调用路径后,官方在3.9版本中剔除了已沦为冗余负担的free_list缓存机制。 视频探讨了连续获取对象方法并查看 `id(o.f)` 时,为何在 Python 3.9 中插入一行 `print` 就会导致前后 ID 从一致变为不一致,而在 Python 3.8 中却始终保持相同的现象。高天解释道,方法属性本质上是通过 descriptor 每次动态生成独立的 `PyMethodObject`,初次测试 ID 相同仅是内存释放后恰好复用的巧合,而 `print` 打乱了内存状态;Python 3.8 始终一致则归功于底层针对 method 的 `free_list` 缓存机制。进一步深挖 CPython 源码演进可知,由于 Python 3.6 引入了 `LOAD_METHOD` 字节码优化使得直接调用方法不再需要构建临时对象,`free_list` 功效减弱并成为累赘,最终在 Python 3.9 中被官方彻底移除。 在 Python 中连续获取同一对象的方法并计算 ID,其结果在 Python 3.9 下未加 print 时相同,但插入一行 print 后就会变成两个不同的地址;而在 Python 3.8 下则始终保持一致。 (0:00) 每次通过属性访问获取方法时,底层的 descriptor 实际上都会新建一个独立的 PyMethodObject,初始测试 ID 相等本质上是生命周期紧密交替导致的内存复用巧合。 (2:50) Python 3.8 针对 method 对象保留了 free_list 内存缓存机制,因而即使插入复杂的 print 扰乱内存,新建方法依然能命中缓存;而 Python 3.9 彻底移除了该机制。 (2:50) 移除 free_list 的深层原因在于 Python 3.6 引入了 LOAD_METHOD 与 CALL_METHOD 字节码优化,直接调用方法无需创建临时对象,导致创建 PyMethodObject 的场景极少,保留缓存反成累赘。 (5:00)

Reading summary

码农高天指出,在 Python 中连续获取对象方法时 `id()` 表现出的一致与不一致并非玄学,而是由于方法 descriptor 机制、临时对象内存复用巧合,以及 CPython 在不同版本中对 `free_list` 优化策略的演化共同决定的。 打印语句与版本差异引发的 ID 谜团 (0:00) 在 Python 3.9 环境中,连续执行 `a = id(o.f)` 与 `b = id(o.f)` 时,两者结果相等;但若在计算 `a` 之后插入一行 `print(a)`,`a` 与 `b` 的值就会变为不相等。而在 Python 3.8 环境下,无论中间是否插入 `print(a)`,多次运行的结果均保持 `a == b`。这种仅仅增加一行打印调试语句或切换 Python 小版本就导致程序表现截然相反的现象,极易被开发者误认为是难以捉摸的“玄学问题”。 Descriptor 生成机制与内存复用巧合 (2:50) 从 Python 对象模型底层来看,访问 `o.f` 本质是通过函数 descriptor 动态创建一个新的 `PyMethodObject`。这意味着两次访问理论上生成的是两个独立的实体对象,内存地址天然不应相同,初始代码表现出的 ID 相等实质上是一种内存分配的巧合。在无干扰的连续调用中,第一个 `o.f` 计算完 `id()` 后生命周期立即结束并释放内存,紧接着生成的第二个 `o.f` 恰好重用了刚刚释放的同一块地址;而插入执行逻辑复杂的 `print(a)` 后,因多次内存分配与回收打乱了原有状态,第二个对象无法再分配到原地址,差异才随之显现。 Python 3.8 的 Free List 缓存行为 (2:50) Python 3.8 与 3.9 的表现差异源于 CPython 内部的内存管理实现。在 Python 3.8 的 `PyMethod_New` 源码中存在一个专用的 `free_list`(空闲列表)缓存机制:当 method 对象销毁时,其内存不会立即归还给操作系统,而是暂存在 `free_list` 中供下一次创建 method 时直接复用。由于该缓存机制专门服务于方法对象,即便中途执行 `print` 扰乱了通用内存分配,后续生成的 `o.f` 依然能从 `free_list` 命中原地址,导致 Python 3.8 下两者的 ID 始终保持一致。 调用优化使 Free List 成为冗余并被移除 (5:00) Python 3.9 之所以移除了 method 的 `free_list`,根源在于 Python 3.6 对方法调用的底层优化。过去常规调用需要构建完整的 `PyMethodObject`,但在 Python 3.6 引入 `LOAD_METHOD` 与 `CALL_METHOD` 字节码指令后,底层通过 `_PyObject_GetMethod` 直接将原始函数指针与 `self` 压栈调用,完全跳过了创建临时方法对象的步骤。由于实际代码中绝大多数都是直接调用(如 `o.f()`),单独提取方法并赋值(如 `a = o.f`)的场景极少,运行时创建临时方法对象的需求大幅缩减,维护 `free_list` 的管理成本反而超过了其优化收益,因此 Python 3.9 核心团队最终将其彻底清除。 码农高天总结认为,看似匪夷所思的调试怪象背后是严谨的底层机制演进;开发者面对难以预测的语言行为时应当深究其运行原理与工程权衡,用科学解释替代玄学猜想。

Source quotes

[5:00] 码农高天: 任何代码都是有代价的,当你一段优化的代码功效变小的时候,这段优化的代码可能就反而成为了累赘。

Original transcript

[0:00] 这期视频我们来聊一个看似玄学的现象这次这个话题是来源于前一阵我的B站一个粉丝问的我一个拍桑的问题我把它精简了一下 然后精简成现在这个状态大家看左边我们有一些代码我首先建立了一个class c 这个class c 里面有个method f然后我又建立了一个class c的object o好有趣点在下面首先我让a等于o.fid我再让B等于O.F的ID第一个问题A跟B相等吗?这个时候你的大脑里可能是一片空白完全想不到这个东西的陷阱所在也可能因为你看过我之前一期的视频,觉得这块有个陷阱那我们运行一下看看结果可以看到A跟B是一样的这时候可能有人欢喜有人愁对吧有人觉得怎么会这样呢有人觉得就是应该这样啊别着急 玄学马上就要来了我不知道大家在debug的时候有没有过这种经历这个bug 我加了一个print之后 它就没法复现了我们今天就来表演一下这个场面大家看我在a=id o.f之后打印了一个a剩下的代码一点都没变我们再运行一下这个程序可以看到神奇的事情发生了我们上来打印了一个A没有问题可是当我们再打印AB的时候我们会发现A跟B竟然不一样了那你知道这背后到底发生了什么事吗好 我相信很多人在实际写程序的过程中 到这个时候就已经认为这是玄学了但这显然不是我们视频的风格对吧我们一定要给他研究明白这个玄学到底是怎么发生的但是在我们研究这个玄学之前我们还要给大家展示一个更悬的事情在我现在使用的virtual environment里面,我的python版本是3.9我们用python3.8来运行一下这程序可以看到结果又不一样了A跟B又一致了当然有些人可能觉得有时候一致有时候不一致对吧我们把这个程序运行若干变 可以看到在python3.8里A跟B永远是一致的而在python3.9里,a跟b永远是不一致的血液学的部分展示完了 让我们回归科学我们先来想一下O.F是什么只要你不是第一次看到我的视频,你就应该记得在前几期视频中,我们刚刚讲过这个function是怎么变成method的我们说每一次在一个object尝试拿它的method时候,实际上都会建立一个新的py method object。也就是说,我们在运行o.f的时候,这个f函数自带的descriptor会新建一个method object返回回来。换言之,O.F本来就应该每一次都不一样。这就是为什么我在刚刚开始提到这个问题的时候说 可能会有人发现这里面的陷阱因为这个问题我之前是讲过的如果你学的比较认真你应该知道O.F跟O.F它是不一样的它是两个object但是如果你意识到了这件事儿 你就会发现你刚开始那道题答案是错的因为在当前这个程序下A跟B就是相等的那这里怎么解释呢就是两个字叫巧合这个巧合里面涉及了大量的Python的内存管理的机制简单的说呢就是由于第一个O.F跟第二个O.F离得足够近所以第一个O.F在使用之后释放的内存又被第二个O.F使用了而在增加了printA这个代码之后,print本身实际上是比较复杂的所以它打乱了python内存管理 导致第二个o.f使用的内存和第一个o.f不一样了于是A跟B就不同了实际上我们也可以通过其他的方式打乱内存管理比如说我们让 x=o.f我们又建立了一个method 并且把它保存到了x里这个时候它的内存就不会被回收我们在运行会发现A跟B依然不一样好 那接下来就有人问了 为什么在python3.8的时候 a跟b是一样的呢这个问题我们就要 [5:00] 回到python的原代码来解释了我们看在python3.8版本的py method new这个函数里面这里有一个freelist的概念freelist什么意思呢就相当于我在建立了一个method之后本来应该释放这个method的时候我先不释放我留着这块内存这样等下一次我又想新建一个method的时候我就不需要重新再去申请内存了我可以从之前剩下的内存里面直接拿而这个机制就使得在我们两次建立method之间加入了一些打乱内存管理的代码依然不会让两个method ID不一样因为在第一个method被release之后,这块内存被保存下来了我们后面用到的依然是那一块内存大家可以对比一下在python3.9的版本里那一段代码就消失了因此我们刚才那个程序在python3.8和python3.9里面行为是不一样的那如果是一般的视频呢这块就要下结论了告诉大家在遇到玄学的时候不要总觉得它玄要到它的背后找它深层次的原因去分析对不对但是,看到这里所有的人心中一定都会有一个疑问这段代码为什么没了呢因为听起来把之前method使用的内存保留下来以后接着用是一个挺好的事情啊它会节省我们反复申请和释放内存的时间为什么要把这段代码删掉呢那这种问题呢就是你关注我的意义所在因为正常人绝对不会跟你讲这么深为什么这段代码消失了 我们要先往回退退到上一个视频,我讲function变成method那里在我们上一个视频里面我们提到这个function都是要通过descriptor变成method执行的这个视频里面就有人留言问这不是一个非常慢的事情吗它难道不会影响性能吗因为你每一次调用一个object method,你都要新建一个method object这件事不蠢吗?答案是蠢。这也就是为什么从py down 3.6开始这件事情被优化了大家看我们左边A等于O点F这里是拿到的这个method而B=O.F是调用了这个method那在我们实际编程的过程中绝大部分情况我们是对这个method进行调用对不对我们来到右边看一下他们对应的字节码当我们做a=o.f的时候 我们用的是loadattr这个部分我们讲过就会使用这个function的descriptor去真的建立一个py method object然后保存到A里但是我们看下面B=O.F的时候 这里我们使用的是load method跟 call method而不是loadattr之后用了一个call function这就是从python3.6开始专门针对method进行的优化我们看这里是load method的原代码在前面他调用了一个py object get method而这个py object get method 函数在找到了这个method 并且确认它是一个method 情况下,它会直接把这个descriptor给保存到Math的指针里而在load method代码里他就可以直接把这个descriptor放到stack里然后再把self放到stack里接下来等调用就可以了而这个我们刚才说的descriptor就是我们在class里面定义的那个f函数也就是说通过这种形式它就避免了新建一个临时的method object而是返回type直接找到函数并且调用它换言之,当你调用method的时候,是没有你想象的那么慢的他并不会真的回去调用descriptor建立一个新的method object然后再返回来你再调用他不是这样的只有你要先保存这个method再重新调用才会走这条线那也正是由于这个优化使得在python里新建method这个事情变得非常的少了而我们知道任何代码都是有代价的当你一段优化的代码功效变小的时候这段优化的代码可能就反而成为了累赘所以在python [10:00] 9版本里面那一段优化的代码就被拿掉了而这一段被拿掉的代码也成为了我们今天玄学的一部分好 那这期视频就到这里 希望大家有所帮助 如果喜欢的话 记得点一下关注 别忘了一键三连哦优优独播剧场——YoYo Television Series Exclusive