码农高天
Python类声明本质上是在独立命名空间中执行代码体并收集局部变量,最终交由底层type构造器绑定为类属性,与运行时直接调用三参数type函数完全等价。
AI-assisted summary of 【python】你知道定义class背后的机制和原理嘛?当你定义class的时候,python实际运行了什么呢? — original source. Check the original for full context.
Source published:
Key ideas
- Python 中的 class 定义在底层并非特殊黑盒,而是将类体包装为一个独立的 code object 并在临时命名空间中执行的可运行代码。 (0:00)
- CPython 解释器通过 `LOAD_BUILD_CLASS` 调用 `builtins.__build_class__`,默认使用元类 `type` 来完成类对象的装配。 (5:00)
- 类体内产生的所有局部变量会被写入临时字典,并最终拷贝绑定到底层 C 结构的 `tp_dict`(即 Python 层的 `__dict__`)中供后续属性查找。 (5:00)
- 静态 `class` 语法与动态调用 `type(name, bases, dict)` 在底层完全等价,三参数 `type` 是 Python 设计中保留的历史特性。 (10:00)
Detailed notes
## Background 该视频由 YouTube 频道“码农高天”于 2023 年 10 月 30 日发布。视频简介指出,本期内容聚焦 Python 中定义 class 背后的底层运行机制,通过解析 CPython 源码说明类的定义本质上只是一段普通的可运行 Python 代码,破除面向对象语法的神秘感。
## TL;DR 视频首先通过字节码拆解了一个极简 Python 类的定义过程,展示类体代码如何被编译为独立的 code object 并通过内置的 __build_class__ 进行调用。随后深入 CPython 源码,详细剖析了 builtin___build_class__ 如何将类体作为局部环境执行、把生成的局部变量写入命名空间字典,并最终通过元类 type 的底层实现(type_call 与 type_new)将字典挂载为类的 tp_dict。最后,作者演示了使用动态三参数 type(name, bases, dict) 手动构建类的方法,阐明静态 class 语法与底层动态建类完全等价的本质。
## Atmosphere & style 视频风格硬核严谨却不失轻松通俗,呈现出极具个人特色的技术漫谈与源码研读氛围。高天以极简的代码示例为切入点,通过字节码和 CPython 源码的逐行追踪,将看似高深的解释器内部细节抽丝剥茧、层层拆解。整期内容充满“知其所以然”的解密快感与探究乐趣,既有干货满满的硬核分析,又带有编程老手化繁为简的亲和力与自信。
## Bulletin - [00:00](https://www.youtube.com/watch?v=2MB-fkS3b70&t=0s) Python 中的 class 定义在底层并非特殊黑盒,而是将类体包装为一个独立的 code object 并在临时命名空间中执行的可运行代码。 - [05:00](https://www.youtube.com/watch?v=2MB-fkS3b70&t=300s) CPython 解释器通过 `LOAD_BUILD_CLASS` 调用 `builtins.__build_class__`,默认使用元类 `type` 来完成类对象的装配。 - [05:00](https://www.youtube.com/watch?v=2MB-fkS3b70&t=300s) 类体内产生的所有局部变量会被写入临时字典,并最终拷贝绑定到底层 C 结构的 `tp_dict`(即 Python 层的 `__dict__`)中供后续属性查找。 - [10:00](https://www.youtube.com/watch?v=2MB-fkS3b70&t=600s) 静态 `class` 语法与动态调用 `type(name, bases, dict)` 在底层完全等价,三参数 `type` 是 Python 设计中保留的历史特性。
## Key takeaways - [00:00](https://www.youtube.com/watch?v=2MB-fkS3b70&t=0s) 类定义体内的成员方法编译出的 code object 与顶层定义的普通函数在底层字节码结构上没有任何区别。 - [00:00](https://www.youtube.com/watch?v=2MB-fkS3b70&t=0s) 类体会被编译为一个独立的 code object,执行时会隐式设置内置变量 `__module__` 与 `__qualname__`。 - [00:00](https://www.youtube.com/watch?v=2MB-fkS3b70&t=0s) 外层字节码通过 `LOAD_BUILD_CLASS` 加载 `builtins.__build_class__` 并传入类体函数和类名来启动类构建流程。 - [05:00](https://www.youtube.com/watch?v=2MB-fkS3b70&t=300s) 在未显式指定 metaclass 且无特殊基类的情况下,`__build_class__` 默认使用元类 `type`(C 源码中的 `&PyType_Type`)。 - [05:00](https://www.youtube.com/watch?v=2MB-fkS3b70&t=300s) 所有在 Python 中直接通过 `class` 语法定义的类对象,其类型本身全部都是 `type`。 - [05:00](https://www.youtube.com/watch?v=2MB-fkS3b70&t=300s) CPython 会初始化一个名为 `ns` 的空字典作为 `locals` 传入类体执行环境,类内的属性赋值与方法声明均直接写入该字典。 - [05:00](https://www.youtube.com/watch?v=2MB-fkS3b70&t=300s) 调用元类 `type` 会触发底层类型槽函数 `tp_call`,进而依次执行 `type_new` 和 `type_init` 完成对象的实例化。 - [05:00](https://www.youtube.com/watch?v=2MB-fkS3b70&t=300s) 底层的 `type_new` 函数将传入的局部变量字典拷贝并赋值给新建类型的 `tp_dict`,该字段即 Python 层通过 `__dict__` 访问的映射表。 - [10:00](https://www.youtube.com/watch?v=2MB-fkS3b70&t=600s) 类定义的本质是先执行类体代码收集所有局部变量,再将键值对存入 `__dict__` 并通过 `type` 构造类对象。 - [10:00](https://www.youtube.com/watch?v=2MB-fkS3b70&t=600s) 动态建类需要三个核心要素:类名字符串、基类元组以及包含属性和方法的命名空间字典。 - [10:00](https://www.youtube.com/watch?v=2MB-fkS3b70&t=600s) 使用 `type(name, bases, dict)` 动态建类与使用 `class` 语法定义的类在底层调用完全相同的 C 实现。 - [10:00](https://www.youtube.com/watch?v=2MB-fkS3b70&t=600s) 单参数 `type(obj)` 用于查询对象类型,三参数 `type(name, bases, dict)` 用于创建新类,这种双重含义属于 Python 的历史包袱。
## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=2MB-fkS3b70&t=0s) 字节码层面的 class 定义:从内层函数到 `__build_class__`
在日常编写 Python 时,开发者几乎每天都在定义类,但 Python 解释器在背后到底执行了哪些操作,很多人并不清楚。码农高天在左侧屏幕展示了一段仅有四行代码的极简类定义:定义了一个名为 `A` 的类,包含一个成员变量 `name = 'AA'` 和一个打印 `1` 的成员方法 `f`。然而,这段简短的代码在 Python 解释器中所生成的字节码结构却并不简单。
```mermaid flowchart TD accTitle: Python 类定义执行流程 accDescr: 展示 Python 解释器从解析 class 内部代码到调用 build_class 并最终返回 type 的执行路径。
subgraph Phase1["1. 解析内部结构"] CodeF["code object: f<br/>(普通函数逻辑)"] CodeA["code object: A<br/>(类体内执行环境)"] end
subgraph Phase2["2. 外层调用准备"] LoadBC["LOAD_BUILD_CLASS<br/>(从 builtins 加载函数)"] MakeFunc["MAKE_FUNCTION A<br/>(将 code object A 封装为函数)"] end
subgraph Phase3["3. 运行并生成类"] CallBC["CALL_FUNCTION<br/>(执行 __build_class__)"] StoreA["STORE_NAME A<br/>(将返回的 type 保存为变量 A)"] end
CodeF --> CodeA CodeA --> MakeFunc LoadBC --> CallBC MakeFunc --> CallBC CallBC --> StoreA ```
从字节码结构来看,整个定义过程可以拆分为三个层次: 1. **最内层的成员方法 `f`**:位于最底部的 `code object f` 是最简单的部分。在类内部定义的函数,在底层实际上与在类外部直接定义的普通函数没有任何区别,它同样是一个独立的 `code object`。在类外部写一个接收参数并打印 `1` 的函数,编译出来的字节码与其完全一致。 2. **中间层的类代码块 `code object A`**:对于很多人来说,这一段字节码相对较难理解。码农高天解释道,它本质上“相当于 class A 冒号里面的内容,它单独拿出来变成了一小段程序”。在这段程序执行业务逻辑前,解释器先做了一些准备工作:设置了内置变量 `__module__ = __name__` 以及 `__qualname__ = 'A'`。接着执行第 2 行的赋值 `name = 'AA'`,并在接下来的字节码中创建了一个名为 `A.f` 的函数对象,将其保存在当前环境的局部变量 `f` 中。换句话说,整个类体可以看作是一个拥有独立局部变量命名空间的小函数,其运行过程就是为其中的局部变量赋值。 3. **最外层的类构建逻辑**:在顶层字节码中,首先出现的是 `LOAD_BUILD_CLASS`。查阅官方文档可知,这一步是将内置模块 `builtins` 中的 `__build_class__` 函数推入栈顶。接着将 `code object A` 封装成一个名为 `A` 的函数对象,然后调用 `CALL_FUNCTION` 执行刚才载入的 `__build_class__`,最后将执行返回的结果保存到名为 `A` 的变量中。码农高天在此提前剧透:“这个值可以提前剧透给大家,是一个 type。”
虽然整体脉络清晰,但一个关键疑问随之产生:类体函数在运行时仅仅是对其内部的局部变量进行赋值,这些局部变量究竟是如何脱离临时的局部作用域,最终成为 `A` 这个类的属性与方法的?
- 类体内定义的方法 `f` 本质上就是普通的 `code object`,与顶层定义的函数没有区别。 - 类体代码被打包成独立的 `code object A`,执行时会隐式设置 `__module__` 和 `__qualname__`。 - Python 解释器通过 `LOAD_BUILD_CLASS` 获取 `builtins.__build_class__`,并将类体函数和类名字符串作为参数传入执行。
---
[05:00](https://www.youtube.com/watch?v=2MB-fkS3b70&t=300s) CPython 源码探秘:`__build_class__` 与 `type` 对象的实例化
带着关于局部变量如何挂载的疑问,码农高天带领观众进入了喜闻乐见的 CPython 源码阅读环节,深入剖析 `builtins` 模块中的 `builtin___build_class__` 实现。
传入 `__build_class__` 的参数主要包括两个:类体生成的函数对象 `func` 和表示类名的字符串 `name`。在 C 源码中,函数先将这两个参数分别解包取出;如果存在继承关系,还会把基类列表存入 `mro` 或 `bases` 变量中。跳过复杂的元类处理后,代码在默认无基类的情况下,会将元类 `meta` 赋值为 `&PyType_Type`(即 Python 中的 `type`)。码农高天强调:“我们所有在 Python 里面直接通过 class 什么什么定义的 class,class 本身的 type 都叫 type。”在代码中直接打印 `type(A)`,验证了类对象本身的类型确实就是 `type`。
随后,源码进入关键的命名空间构建与代码执行阶段: - **创建命名空间字典 `ns`**:在基础场景下,变量 `ns` 被直接初始化为一个空的 Python 字典(`dict`),`ns` 即 `namespace` 的缩写。 - **执行类体字节码(`execute`)**:源码在 222 至 224 行执行了类体对应的 `code object`,并把字典 `ns` 作为保存局部变量(`locals`)的映射空间直接传入。码农高天形象地指出,这在 Python 层面相当于“新开了一个小环境”或者做了一次 `exec` 操作。在 `name = 'AA'` 和 `def f(): print(1)` 执行完毕后,产生的局部变量以键值对的形式被悉数写入 `ns`:包含了键 `'name'` 对应值 `'AA'`,以及键 `'f'` 对应新生成的函数对象 `f`。
```mermaid flowchart LR accTitle: CPython type 实例化与字典绑定 accDescr: 展示 namespace 字典如何通过 type_call 和 type_new 最终绑定到类对象的 tp_dict 上。
subgraph Execution["1. 局部环境执行"] NS["ns (空 dict)"] --> Exec["运行 code object A"] Exec --> FilledNS["ns 写入键值对:<br/>'name': 'AA', 'f': <func>"] end
subgraph TypeCall["2. 调用元类 type"] FilledNS --> VCall["vectorcall 调用 meta (type)"] VCall --> TCall["type_call (tp_call)"] end
subgraph TypeNew["3. 底层结构装配"] TCall --> TNew["type_new (tp_new)"] TNew --> CopyDict["copy ns -> dict"] CopyDict --> Bind["赋给 type->tp_dict<br/>(即 A.__dict__)"] end ```
接下来便进入了真正产生类或类型的核心步骤:源码构建了一个包含 `name`、`bases` 和 `ns` 的参数数组,发起 `vectorcall` 调用元类 `meta`(即 `type`)。因为 `type` 本身也是一个 Python 对象,当调用它时会触发其类型槽函数 `tp_call`(指向 C 源码中的 `type_call`)。在 `type_call` 中,依次触发了 `type->tp_new`(在底层对应 `type_new` 函数)以及 `type->tp_init`: - 在 `type_new` 函数中,参数被依次解析,若未指定基类则默认继承 `PyBaseObject_Type`(即 `object`)。 - 关键逻辑出现在 2454 行前后,源码将传入的原始命名空间字典拷贝一份存入 `dict`。 - 在 2643 行,这个 `dict` 被正式赋值给新建类型的 `tp_dict` 字段。码农高天揭秘道,这个 `tp_dict` 就是 Python 中的 `__dict__`;当后续在实例或类上访问 `a.f` 时,解释器正是通过该字典找到对应的函数对象。
- `builtin___build_class__` 接收类体函数和类名,若未显式指定 metaclass,默认使用 `type`。 - 类体代码执行时,将一个空的 Python 字典作为 `locals` 传入,所有变量定义和方法声明直接写入该字典。 - `type` 的调用会触发 C 语言底层的 `type_call`,进而调用 `type_new`(对应 Python 层的 `__new__`)。 - 类属性字典 `tp_dict` 直接由传入的 `ns` 拷贝并绑定,构成了后续属性查找的基础。
---
[10:00](https://www.youtube.com/watch?v=2MB-fkS3b70&t=600s) 运行验证与动态建类:`type` 的三个要素与历史包袱
为了验证上述机制,码农高天在代码中打印了 `A.__dict__`。输出结果清晰地证实了底层逻辑:其内部包含 `__module__: '__main__'`、`name: 'AA'` 以及指向函数 `<function A.f>` 的 `f`,与字节码分析的结果完全吻合。
码农高天对类定义的本质进行了通俗总结:“当我们定义一个新的 class 的时候,首先相当于运行了所有在这个 class 里面的代码,然后把产生的所有局部变量的名字和他们对应的值都保存到了这个 class 的 `__dict__` 里面,接着我们建立了一个 type,做很多我们今天没有讲的事情,然后把这个 type 赋值给了 class 名字的变量。”
由此引申出一个核心结论:类定义并不是什么神奇的特殊语法,其内部就是一段普通的可执行代码。除了通过 `class A:` 进行静态定义外,开发者完全可以在运行时动态创建一个类。动态构建类只需要三个核心要素: 1. **类的名字(name)**:字符串,如 `'A'`。 2. **父类元组(bases)**:包含所有基类的元组,若不继承其他自定义类则传入空元组 `()`。 3. **命名空间字典(dict)**:保存属性与方法的字典,例如 `{'name': 'AA', 'f': f}`。
利用内置的 `type` 函数,传入这三个要素:`type('A', (), {'name': 'AA', 'f': f})`,所得到的结果与使用 `class` 语法定义的类完全等价,二者在底层调用的甚至就是同一个 C 函数。
最后,码农高天解释了 Python 语法中一个常见的困惑:为什么有时 `type(x)` 查类型只传一个参数,有时却能传三个参数动态建类?“type 这个东西他在 take 一个 argument 的时候返回的是这个东西的 type,而在 take 三个 argument 的时候他返回的是一个新的 type,这件事其实是一个 Python 的历史包袱。”虽然动态建类在实际日常业务开发中并不算十分常见,但理解这一底层统一性,能够帮助开发者彻底剥除面向对象语法的神秘面纱。
- 静态定义 `class` 与动态调用 `type(name, bases, dict)` 底层逻辑完全等价。 - 动态建类三要素:类名字符串、父类元组、包含属性和方法的命名空间字典。 - 单参数 `type(obj)` 返回对象类型,三参数 `type(name, bases, dict)` 创建新类,这是 Python 设计保留的历史特性。
## Notable quotes > [10:00](https://www.youtube.com/watch?v=2MB-fkS3b70&t=600s) type 这个东西他在 take 一个 argument 的时候返回的是这个东西的 type,而在 take 三个 argument 的时候他返回的是一个新的 type,这件事其实是一个 Python 的历史包袱。 — 码农高天
> [05:00](https://www.youtube.com/watch?v=2MB-fkS3b70&t=300s) 我们所有在 Python 里面直接通过 class 什么什么定义的 class,class 本身的 type 都叫 type。 — 码农高天
## Mentioned Python, CPython, 码农高天, type, builtins, __build_class__, PyType_Type, PyBaseObject_Type
## Video description 这期视频聊一下class,主要是定义class背后的机制,让大家知道,class的定义,并不是什么特殊的神奇的事情,它也就是一段可运行的python代码而已。又有大家喜闻乐见的读cpython源码环节啦!
[Source](https://www.youtube.com/watch?v=2MB-fkS3b70)