CPython底层拆解:Code Object以整数数组索引消解动态查找开销

【python】B站没人讲过的CodeObject,python底层实现一点都不简单! — 码农高天

码农高天

Python在编译期将变量名映射为连续数组下标并根据作用域拆分出独立元组,使虚拟机免于在运行时维护哈希表即可实现高效的指令执行与闭包共享。

AI-assisted summary of 【python】B站没人讲过的CodeObject,python底层实现一点都不简单! — original source. Check the original for full context.

Source published:

Key ideas

  • 所有 Python 代码在运行前都会被编译为 Code Object,它包含了二进制字节码、虚拟机执行配置以及高度优化的行号映射元数据。 (0:00)
  • Python 虚拟机为了避免在运行时进行低效的字符串哈希查找,在编译期将变量名映射为静态整数索引,字节码直接通过数组下标实现 O(1) 访问。 (10:00)
  • 闭包机制通过成对的 co_cellvars 与 co_freevars 实现:外层函数将跨作用域共享的变量封装为 cell object,字节码指令也随之从普通 FAST 指令切换为 DEREF 指令。 (15:00)
  • Code Object 的常量池 co_consts 直接存储真实的 Python 对象而非字符串名字,且 None 默认始终常驻在常量元组中。 (15:00)

Detailed notes

## Background 本视频由技术博主“码农高天”于 2023 年 9 月 29 日发布在 YouTube 平台。视频简介指出,本期内容深入硬核剖析了所有 Python 代码在运行前编译成的 Code Object(代码对象),旨在梳理其内部包含的复杂组成与底层机制,并直言官方文档在这一主题上的阐述都未必清晰透彻。

## TL;DR 视频系统拆解了 Python 运行时的底层基石 Code Object,将其核心结构划分为二进制字节码、元数据、虚拟机执行配置以及参数统计四大类别。高天详细阐述了 Python 如何利用斜杠(/)与星号(*)区分仅限位置参数与仅限关键字参数,并映射到底层的不同参数计数属性中。随后,他从字节码设计原理推导了 Python 为什么将变量名映射为静态整数下标以优化访问性能,并澄清了容易混淆的 co_varnames、co_name 与 co_names 的实际职责。最后,视频通过嵌套函数与反编译实例,深度揭示了闭包机制下成对协作的 co_cellvars、co_freevars 与 Cell Object 底层指针封装,并完整呈现了固化 Python 对象的常量池 co_consts。

## Atmosphere & style 整期视频散发着纯粹的硬核极客气质与技术自信,语言风格直率幽默,既带有“连官方文档都没讲透”的犀利切入,也有对个别历史命名缺陷的坦率吐槽。高天不仅停留在打印属性的表象,而是带领观众站在语言设计者的视角推导虚拟机的实现权衡,结合即时的代码验证与 dis 字节码反编译层层推演。观看体验严谨烧脑却又脉络清晰,犹如置身于资深系统级工程师的白板演练现场,令人酣畅淋漓地看透 Python 虚拟机内部的精密齿轮。

## Bulletin - [00:00](https://www.youtube.com/watch?v=2ifX3yAnAFc&t=0s) 所有 Python 代码在运行前都会被编译为 Code Object,它包含了二进制字节码、虚拟机执行配置以及高度优化的行号映射元数据。 - [10:00](https://www.youtube.com/watch?v=2ifX3yAnAFc&t=600s) Python 虚拟机为了避免在运行时进行低效的字符串哈希查找,在编译期将变量名映射为静态整数索引,字节码直接通过数组下标实现 O(1) 访问。 - [15:00](https://www.youtube.com/watch?v=2ifX3yAnAFc&t=900s) 闭包机制通过成对的 co_cellvars 与 co_freevars 实现:外层函数将跨作用域共享的变量封装为 cell object,字节码指令也随之从普通 FAST 指令切换为 DEREF 指令。 - [15:00](https://www.youtube.com/watch?v=2ifX3yAnAFc&t=900s) Code Object 的常量池 co_consts 直接存储真实的 Python 对象而非字符串名字,且 None 默认始终常驻在常量元组中。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=2ifX3yAnAFc&t=0s) Python 函数本质上是 Function Object,每个函数对象内部都持有一个对应的 Code Object(可通过 f.__code__ 访问)。 - [00:00](https://www.youtube.com/watch?v=2ifX3yAnAFc&t=0s) co_code 以二进制格式保存 Python 的底层字节码,开发者可通过标准库 dis 模块将其反编译为可读的汇编助记符。 - [00:00](https://www.youtube.com/watch?v=2ifX3yAnAFc&t=0s) co_lnotab(行号映射表)在 CPython 中经过了极致的体积压缩与优化,负责在二进制字节码与源码行号之间建立映射。 - [00:00](https://www.youtube.com/watch?v=2ifX3yAnAFc&t=0s) co_flags 是一个位图(Bitmap),用于标识函数是否包含变长参数、是否为生成器或协程,引导虚拟机采取不同的执行模式。 - [05:00](https://www.youtube.com/watch?v=2ifX3yAnAFc&t=300s) Python 形参语法通过斜杠 / 定义 Position-only 参数(对应 co_posonlyargcount),通过独立星号 * 定义 Keyword-only 参数(对应 co_kwonlyargcount)。 - [05:00](https://www.youtube.com/watch?v=2ifX3yAnAFc&t=300s) co_argcount 统计的是扣除了变长参数(*args、**kwargs)以及全部 Keyword-only 参数后剩余的常规参数数量。 - [10:00](https://www.youtube.com/watch?v=2ifX3yAnAFc&t=600s) 如果在字节码中直接存储变量名字符串,会导致字节码膨胀且运行时查询开销过大,因此 Python 采用编译期名字索引表配合 LOAD_FAST/STORE_FAST 指令。 - [10:00](https://www.youtube.com/watch?v=2ifX3yAnAFc&t=600s) co_varnames 存储所有的本地局部变量名(包含形参),其数量通常对应 co_nlocals。 - [10:00](https://www.youtube.com/watch?v=2ifX3yAnAFc&t=600s) co_name 与 co_names 概念截然不同:co_name 是函数自身的名字,而 co_names 保存作用域内使用的属性名、全局对象名或模块导入名。 - [10:00](https://www.youtube.com/watch?v=2ifX3yAnAFc&t=600s) import 语句在无别名时会同时向 co_names(模块查找)和 co_varnames(局部绑定)写入模块名;使用 as 别名后原模块名仅保留在 co_names 中。 - [15:00](https://www.youtube.com/watch?v=2ifX3yAnAFc&t=900s) co_cellvars 记录本层作用域中被内层嵌套闭包所引用的变量名,底层会将这些变量从常规栈帧剥离并封装为 cell object。 - [15:00](https://www.youtube.com/watch?v=2ifX3yAnAFc&t=900s) co_freevars 记录内层嵌套函数从外层非全局作用域捕获的自由变量名,虚拟机通过 LOAD_DEREF 指令对其进行解引用读取。 - [15:00](https://www.youtube.com/watch?v=2ifX3yAnAFc&t=900s) co_consts 保存代码涉及的所有字面常量实体对象(如整数、字符串、None),虚拟机执行 LOAD_CONST 时直接按索引载入。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=2ifX3yAnAFc&t=0s) 什么是 Code Object:字节码、元数据与虚拟机配置

高天指出,Code Object(代码对象)在 Python 中是一个极其硬核但又底层的概念。他翻阅了 B 站后发现甚至没有一个视频专门讲解这个主题,官方文档对它的描述也相当模糊,但它对理解 Python 的底层运行机制至关重要。高天直言:“不是我夸张,一般人真的搞不明白。你去翻官方文档,都未必比我讲得清楚。” 许多使用了 Python 多年的开发者可能从未听过这个词,但只要在写 Python,其实时时刻刻都在与它打交道——所有编写的 Python 代码在运行前都会被编译为 Code Object。如果定义一个空的函数 `f`,通过打印其内部属性 `f.__code__`,就能拿到这个 Code Object。函数本身是一个 Function Object,而每个 Function Object 都有一个与之对应的 Code Object。

通过标准库的 `inspect` 模块文档以及直接在程序中打印对象属性,可以发现 Code Object 包含了一系列核心属性,高天将其分门别类进行了拆解:

1. **`co_code`**:保存的是该代码段真正的字节码(Bytecode),在底层以二进制(Binary)形式存在。高天解释称通俗理解这就是 Python 的“汇编”,普通开发者通常不直接读取这段二进制,而是通过内置的 `dis` 模块(如 `dis.dis(f)`)将其反编译为人类可读的助记符。 2. **元数据属性(Metadata)**:高天将这些属性归类为与实际代码逻辑运行关系不大、起到辅助作用的元数据。 - `co_name`:代码段的名称,通常就是定义的函数名。 - `co_filename`:定义该函数或代码段所在的文件路径。 - `co_lnotab`(或行号映射表):一个将二进制字节码与源代码行号一一映射的表。为了节省空间,CPython 对其进行了高度压缩与优化,人类无法直接阅读;高天提到 CPython 源码中甚至有专门的文本文件(text file)用来解释这种压缩算法。 3. **运行时与虚拟机属性**: - `co_stacksize`:Python 虚拟机(VM)在执行这段代码时所需的栈空间大小。 - `co_flags`:一个位图(Bitmap)。编译器在编译代码时会通过它标记代码的特殊属性,例如是否含有 `*args`、`**kwargs`,或者该函数是否为生成器(Generator)或协程(Coroutine)。Python 虚拟机会根据这些 flag 展现不同的执行行为(例如执行普通函数与生成器函数的逻辑截然不同)。

```mermaid flowchart TD accTitle: Python Code Object 核心属性分类结构图 accDescr: 展示 Code Object 内部的属性如何划分为字节码、元数据、虚拟机状态及参数统计四大类

subgraph CO["Code Object (f.__code__)"] subgraph BytecodeSec["字节码执行"] C1["co_code<br/>(二进制字节码 / dis 模块反编译)"] end

subgraph MetaSec["元数据 (Metadata)"] M1["co_name (函数名)"] M2["co_filename (文件名)"] M3["co_lnotab (字节码到行号压缩映射)"] end

subgraph VMSec["虚拟机状态 (Runtime)"] V1["co_stacksize (栈深度)"] V2["co_flags (Bitmap: 生成器/协程/变长参数标志)"] end

subgraph ArgSec["参数计数 (Argument Counts)"] A1["co_argcount"] A2["co_posonlyargcount"] A3["co_kwonlyargcount"] end end

BytecodeSec --> CO MetaSec --> CO VMSec --> CO ArgSec --> CO ```

---

[05:00](https://www.youtube.com/watch?v=2ifX3yAnAFc&t=300s) 参数计算机制:Position-only 与 Keyword-only Arguments

在进入更复杂的变量名讨论之前,高天重点剖析了 Code Object 中关于参数数量的三个属性:`co_argcount`、`co_posonlyargcount` 和 `co_kwonlyargcount`。这三个计数器决定了 Python 在函数调用时如何校验和传递参数,也是 Python 函数传参机制的核心基础。

为了厘清这三个计数的差别,高天详细梳理了 Python 参数定义的演进与语法: - 常规编写参数包括:必填参数(如 `a`)、带默认值的参数(如 `b=3`)、以及变长参数(`*args` 和 `**kwargs`)。变长参数无论传入多少都会被打包接收。但默认情况下,常规参数既不是纯位置参数,也不是纯关键字参数。 - **Position-only Arguments(仅限位置参数)**:Python 中引入了斜杠 `/` 语法。在函数形参列表中,**斜杠之前**的所有参数都必须以位置参数形式传入,绝对不能以关键字形式传参。例如定义 `def f(a, b=3, /): pass`: - 调用 `f(1)` 合法(`1` 赋值给 `a`,`b` 取默认值 3)。 - 调用 `f(1, 1)` 合法(分别按位置赋给 `a` 和 `b`)。 - 调用 `f(a=1)` 则直接抛出语法或类型异常,报错提示缺少必需的位置参数(required positional argument)。此时若查看 Code Object,其 `co_posonlyargcount` 为 2。只有去掉 `/`,该属性才会变回 0。 - **Keyword-only Arguments(仅限关键字参数)**:在形参列表中放一个单独的星号 `*`,例如 `def f(a, *, b=3): pass`。**星号之后**的所有参数都必须以关键字形式传递。若调用 `f(1, 1)` 会直接失败报错;必须写成 `f(1, b=1)` 才能正常执行。此时 Code Object 的 `co_kwonlyargcount` 变为 1。

由此,高天总结了这三个属性的精准定义: - `co_posonlyargcount`:纯位置参数的个数。 - `co_kwonlyargcount`:纯关键字参数的个数。 - `co_argcount`:排除掉 `*args`、`**kwargs` 以及所有 Keyword-only 参数之后,剩余参数的总数量。

---

[10:00](https://www.youtube.com/watch?v=2ifX3yAnAFc&t=600s) 字节码设计权衡与变量名管理:从 co_varnames 到 co_names

讲完参数数量后,高天进入了他称为“最复杂、最容易混淆,这个世界上都没有几个人搞得明白”的名称属性集合。在解释属性前,高天引导观众先从底层设计者的角度思考一个计算机体系结构问题:**字节码应该如何设计?**

假设函数内部写了一行代码 `b = a`,如果字节码里直接嵌入变量名字字符串 `"a"` 和 `"b"`,会带来严重缺陷:如果变量名极其冗长且频繁出现,字节码体积会急剧膨胀;更关键的是,在运行时 `"a"` 和 `"b"` 代表的是具体的 Python 对象,虚拟机若要通过字符串找到对象,就必须维护哈希表或字典映射,无论在时间复杂度还是空间复杂度上都极其低效。

高天强调了 Python 的破局思路:“在编译代码的时候,这个代码里面出现的变量名也好,或者是任何的名也好,他是有数的,也就是说它完全可以做到一个一一对应。” Python 在编译期维护数组:每见到一个新名字就分配一个整型下标(如 `a` 为 0,`b` 为 1)。字节码只需记录数字指令,例如“读取第 0 号位置的数据,存入第 1 号位置”(即 `LOAD_FAST 0`, `STORE_FAST 1`),从而实现 $O(1)$ 的快速数组索引访问。

```mermaid flowchart LR accTitle: Python 编译期变量名到字节码的静态映射机制 accDescr: 说明源码中的变量名如何被转化为整数索引,从而使字节码直接操作数组下标

subgraph Source["源代码 (AST 解析)"] S1["b = a"] end

subgraph Mapping["静态名字数组 (编译期)"] Arr0["Index 0: 'a'"] Arr1["Index 1: 'b'"] end

subgraph Bytecode["最终字节码指令"] B1["LOAD_FAST 0"] B2["STORE_FAST 1"] end

Source --> Mapping Mapping --> Bytecode ```

在这个机制下,Code Object 分离出了不同的名字属性: - **`co_varnames` 与 `co_nlocals`**: - `co_varnames` 保存的是所有的局部变量名(Local Variables),包括函数传入的形参(arguments)以及在函数本地作用域内定义的变量。 - `co_nlocals` 是局部变量的总数量。绝大多数情况下它等于 `len(co_varnames)`。高天坦诚分享,CPython 源码中对它们的大小做过校验,但他自己也没完全搞明白到底在什么极端特殊情况下这两者会不相等。 - 用 `dis.dis` 查看字节码时看到的 `LOAD_FAST 0 (a)`,括号内的 `(a)` 仅仅是反编译模块为了人类可读性附加的注释,实际二进制底层只存了操作码和操作数 `0`。 - **`co_names` 与 `co_name` 的陷阱**: - 高天特别吐槽道:“`co_names` 和 `co_name` 没有一毛钱的关系,这里这个名字的设计稍微有点愚蠢,它没有任何关系。” `co_name` 是当前函数自身的名字,而 `co_names` 是一个元组,保存了除局部变量(`co_varnames`)、外层闭包变量(`co_cellvars`)和引用的自由变量(`co_freevars`)之外,该作用域内用到的所有其他非保留字名字。 - 例如属性访问 `b = a.attr`,其中的属性名 `"attr"` 就保存在 `co_names` 中,字节码通过 `LOAD_ATTR` 读取;调用类方法 `a.method()` 时,`"method"` 字符串同样存放在 `co_names` 中。 - 高天还举了 `import` 语句的精妙例子:如果写 `import math`,`math` 会同时出现在 `co_varnames` 和 `co_names` 中。在 `co_names` 中是因为导入模块需要 `"math"` 字符串;在 `co_varnames` 中则是因为隐含着将导入的模块对象赋值给本地变量 `math`。如果改成 `import math as m`,则 `"math"` 仅出现在 `co_names` 中,而本地变量名 `"m"` 出现在 `co_varnames` 中。

---

[15:00](https://www.youtube.com/watch?v=2ifX3yAnAFc&t=900s) 闭包机制下的 cellvars 与 freevars,及常量池 co_consts

紧接着,高天拆解了另一对极易混淆的概念——`co_cellvars` 和 `co_freevars`。他用生动的比喻指出:“这两个概念有点像硬币的两面,它们经常成对出现,一般来说都是辅助完成闭包的。” - **`co_cellvars`**:记录的是本作用域(scope)内定义、但**被内部嵌套的其它作用域(闭包)所引用/修改**的变量。在 Python 底层实现闭包时,虚拟机不会把该变量当作普通局部变量存放在快速栈帧中,而是将其包装成一个 `cell object`(可以理解为一种引用或指针封装),使不同的作用域能够共享访问。 - **`co_freevars`**:记录的是当前作用域正在使用、但**定义在外层其他作用域**的变量(即自由变量)。

高天通过一段简单的嵌套闭包代码进行了实机演示: ```python def g(): d = {} def f(): d['key'] = 1 return f ``` - 查看外层函数 `g` 的 Code Object:因为内部函数 `f` 引用了 `d`,变量 `d` 就被放进了 `g.__code__.co_cellvars`。如果修改代码让 `f` 不引用 `d`,`d` 就会立刻退化回普通的 `co_varnames`。 - 查看内层函数 `f` 的 Code Object:其 `f.__code__.co_freevars` 中包含了 `('d',)`,代表它是从外层捕获的自由变量。 - 这一区分直接反映在字节码指令上:普通的局部变量读写使用的是 `LOAD_FAST` / `STORE_FAST`;而在闭包捕获场景下,外层使用的是 `STORE_DEREF` 和 `LOAD_CLOSURE`,内层使用的则是 `LOAD_DEREF`。编译器会根据变量的作用域来源自动调整生成的字节码指令。

```mermaid flowchart TD accTitle: 闭包中 cellvars 与 freevars 的对应关系 accDescr: 展示外层函数与内层嵌套函数如何通过 cellvars 和 freevars 共享 cell object

subgraph ScopeG["外层函数 g()"] VarD["定义变量 d = {}"] CellD["co_cellvars: ('d',)<br/>指令: STORE_DEREF / LOAD_CLOSURE"] end

subgraph ScopeF["内层嵌套函数 f()"] UseD["使用变量 d"] FreeD["co_freevars: ('d',)<br/>指令: LOAD_DEREF"] end

CellObj["Cell Object (堆内存指针/引用封装)"]

VarD --> CellD CellD -->|"封装并暴露"| CellObj CellObj -->|"跨作用域捕获"| FreeD FreeD --> UseD ```

最后,高天介绍了属性 **`co_consts`(常量元组)**。与前面存放字符串名字的属性完全不同,`co_consts` 保存的是真正的 **Python Object**。代码中出现的所有字面常量都会被打包固化在里面。例如在函数中写入 `a = 1; b = "ABCABC"`,打印 `co_consts` 会发现 `(None, 1, 'ABCABC')`,其中的 `1` 是整数对象,`'ABCABC'` 是字符串对象。高天特别指出,`None` 是常量池里的“常驻嘉宾”,始终默认存在于其中。Python 虚拟机在运行字节码(如 `LOAD_CONST`)时,就是直接从 `co_consts` 列表中按索引载入这些常量对象。

在视频尾声,高天总结道,虽然这期视频难度偏大,需要理解大量底层虚拟机的抽象概念,但如果开发者真正想弄懂 Python 底层的运行机制,Code Object 是绝对躲不开的一块基石。

- 关键局部变量与名字属性对照: - `co_varnames`:函数参数与本地局部变量名。 - `co_names`:除局部/闭包变量外,所有被引用的属性名、导入名或全局对象名。 - `co_cellvars`:本地创建并被内部嵌套闭包引用的变量名。 - `co_freevars`:引自外层非全局作用域的变量名。 - `co_consts`:代码中包含的实际 Python 常量对象集合(第一项通常是 `None`)。

## Notable quotes > [10:00](https://www.youtube.com/watch?v=2ifX3yAnAFc&t=600s) co_names 和 co_name 没有一毛钱的关系,这里这个名字的设计稍微有点愚蠢,它没有任何关系。 — 高天

## Mentioned 高天, Python, CPython, B站, Code Object, Function Object, inspect, dis

## Video description 这期视频讲个硬核的内容,Code Object。Python的代码最终都会被编译成code object,那code object里包含了什么内容呢?不是我夸张,一般人真的搞不明白。你去翻官方文档,都未必比我讲得清楚。

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

中文简报

CPython底层拆解:Code Object以整数数组索引消解动态查找开销 Python在编译期将变量名映射为连续数组下标并根据作用域拆分出独立元组,使虚拟机免于在运行时维护哈希表即可实现高效的指令执行与闭包共享。 视频系统拆解了 Python 运行时的底层基石 Code Object,将其核心结构划分为二进制字节码、元数据、虚拟机执行配置以及参数统计四大类别。高天详细阐述了 Python 如何利用斜杠(/)与星号(*)区分仅限位置参数与仅限关键字参数,并映射到底层的不同参数计数属性中。随后,他从字节码设计原理推导了 Python 为什么将变量名映射为静态整数下标以优化访问性能,并澄清了容易混淆的 co_varnames、co_name 与 co_names 的实际职责。最后,视频通过嵌套函数与反编译实例,深度揭示了闭包机制下成对协作的 co_cellvars、co_freevars 与 Cell Object 底层指针封装,并完整呈现了固化 Python 对象的常量池 co_consts。 所有 Python 代码在运行前都会被编译为 Code Object,它包含了二进制字节码、虚拟机执行配置以及高度优化的行号映射元数据。 (0:00) Python 虚拟机为了避免在运行时进行低效的字符串哈希查找,在编译期将变量名映射为静态整数索引,字节码直接通过数组下标实现 O(1) 访问。 (10:00) 闭包机制通过成对的 co_cellvars 与 co_freevars 实现:外层函数将跨作用域共享的变量封装为 cell object,字节码指令也随之从普通 FAST 指令切换为 DEREF 指令。 (15:00) Code Object 的常量池 co_consts 直接存储真实的 Python 对象而非字符串名字,且 None 默认始终常驻在常量元组中。 (15:00)

Reading summary

高天指出,所有 Python 代码在运行前都会被编译为代码对象(Code Object,可通过函数对象的 `__code__` 属性访问),它是理解 Python 虚拟机运行机制的核心基础。该对象整合了二进制字节码、调试元数据、栈帧执行配置以及静态符号与常量池,构成了 CPython 解释执行的底层骨架。 字节码构成与虚拟机运行时配置 (0:00) Code Object 包含代码实际执行所需的底层逻辑与虚拟机调度信息。其中,`co_code` 以二进制形式存储该代码段的核心字节码(Bytecode),开发者通常需使用标准库的 `dis` 模块将其反编译为人类可读的助记符。在元数据层面,`co_name` 记录函数或代码段名称,`co_filename` 标明源码文件路径,而 `co_lnotab` 则是经过高度压缩的映射表,专门用于在节省内存的前提下将字节码偏移量对齐到源代码行号。 针对执行环境,虚拟机依赖 `co_stacksize` 确定执行该段代码所需的栈空间深度;同时借助位图属性 `co_flags` 标记该代码块的运行特征。编译器会在编译期通过该标志位识别代码是否包含变长参数(`*args`、`**kwargs`),或者是否属于生成器(Generator)与协程(Coroutine),Python 虚拟机据此在运行时直接切换不同的执行流。 参数约束语法与形参计数机制 (5:00) Python 在底层通过三组独立的计数器对函数调用时的实参传递进行静态校验,包括 `co_posonlyargcount`、`co_kwonlyargcount` 和 `co_argcount`。这一设计与 Python 引入的形参分隔语法直接对应:形参列表中斜杠 `/` 之前的参数属于仅限位置参数(Position-only),若以关键字形式调用会直接抛出类型异常,其数量直接计入 `co_posonlyargcount`;而星号 `*` 之后的参数属于仅限关键字参数(Keyword-only),必须通过显式键名传递,其数量由 `co_kwonlyargcount` 统计。 除去上述两类特殊约束以及由虚拟机自动打包的 `*args` 和 `**kwargs` 变长参数外,剩余常规参数的规模与位置参数的总量被统一汇总在 `co_argcount` 中。这三个计数器在函数调用发生时构成了 CPython 校验入参合法性的核心依据。 静态索引优化与名字分类体系 (10:00) 为了避免在字节码中直接硬编码冗长的变量名字符串导致存储膨胀,并规避运行时高频查询字典带来的性能开销,Python 采用了编译期静态数组索引机制。编译器在语法分析阶段将局部变量映射为整数下标,使字节码仅需执行 `LOAD_FAST` 或 `STORE_FAST` 指令即可按数组偏移量实现 $O(1)$ 复杂度的快速读写。这些局部变量名连同函数形参统一保存在 `co_varnames` 中,其总数由 `co_nlocals` 记录。 高天强调,`co_names` 与函数自身的名称 `co_name` 没有任何关联,而是一个独立的名称元组。它存储了当前作用域内除局部变量、闭包变量之外的所有标识符,例如属性访问 `a.attr` 中的 `'attr'`,或者模块调用中的方法名。在模块导入场景下,`import math` 会使 `'math'` 同时出现在 `co_varnames`(本地接收变量)与 `co_names`(模块引用名)中;若改写为 `import math as m`,则 `co_varnames` 记录局部别名 `'m'`,而 `co_names` 仅保留原始模块名 `'math'`。 闭包变量映射与常量池管理 (15:00) 在处理跨作用域访问时,Code Object 通过成对出现的 `co_cellvars` 与 `co_freevars` 维护闭包机制。在外层函数中定义、但被内层嵌套作用域引用或修改的变量会被记录在 `co_cellvars` 中,虚拟机不再将其分配至普通栈帧,而是封装为堆上的 `cell object`(指针引用封装);内层嵌套函数则在其 `co_freevars` 中登记所捕获的外部自由变量。这种区分反映在字节码指令上,变量读写会从普通的 `LOAD_FAST` 切换为针对闭包的 `STORE_DEREF`、`LOAD_CLOSURE` 及 `LOAD_DEREF`。 与前述存放字符串标识符的属性不同,`co_consts` 是直接存储实际 Python 对象的常量池元组。代码中出现的字面量(如整数、字符串对象)会在编译期固化至该元组中供 `LOAD_CONST` 指令读取,而 `None` 作为默认常量通常始终固定占据该池的首位。 高天总结表示,Code Object 是深入理解 Python 虚拟机运作逻辑的关键构件。尽管 CPython 源码中仍存在如 `co_nlocals` 与 `len(co_varnames)` 在极端特殊边界下校验逻辑未完全公开等少数细节,但掌握其字节码指令优化、变量槽分配与闭包封装模型,能够清晰解释 Python 运行时的传参限制与执行行为。

Source quotes

[10:00] 高天: co_names 和 co_name 没有一毛钱的关系,这里这个名字的设计稍微有点愚蠢,它没有任何关系。

Original transcript

[0:00] 这期视频给大家介绍一个硬核的东西叫做CodeObject我简单翻了一下B站 没有一个视频是在讲这个内容的包括他官方的文道说的也比较模糊但是这个东西对于你理解python的底层机制又非常重要然后我又是一个爱啃硬骨头的人所以今天就带大家了解一下这个code object到底是个什么东西好 提到code object 可能很多用了python很多年的人都没有听过这个词啊但是实际上呢 只要你在用python就在跟他打交道我们所有写的python代码在运行的时候都被编译成了code object我们看左边这个例子我们就define了一个空的函数然后只要打印这个函数的下方线扣的就可以看到它是一个code object我们前面其实提到过 对不对 F 本身是一个Function object每一个Function object都会有它对应的一个Code object好 那这个codeobject里保存了什么呢我们看一下官方文档这里是官方文档的inspect里面来介绍的code object里面有的attribute那除了文档之外呢 你也可以在程序里对一下这个object可以看到 Ashgill的部分是一样的那我们把这些attributes来分分类分别讨论一下他们都是干嘛的首先是这个code object的co-code attribute它里面保存的是这一段代码真正的bytecode是用二进制也就是Binary来表示的Bitcode通俗一点理解呢你可以认为这块是它的汇变这个attribute我们一般不直接去接触如果我们要看它的汇编的话更多的时候我们是用this这个module我们之前也演示过很多次了对吧this.this.f它会给你显示出一个人类可读的它的字节码当然在保存在机器上和机器去使用的时候呢它是一个二净值的数据啊接下来这三个属性我管它叫做metadata就是它们和真正的代码运行关系并不大他们是作为辅助数据出现的分别是这段code的名字名字一般就是你定义的函数的名字然后它是在哪一个文件里被定义的第三个相对来说难理解一点第三个它保存的是一个mapping是一个一一对应它把每一个字节码也就是bycode的对应的原代码的行数以二进制的形式保存在这里面了当然了它保存的形式有一定的优化和压缩人类是没有办法直接看懂的如果你感兴趣的话 在cpython的原本里面有整整一个taxfile来给你讲述这个东西是怎么来压缩的这个不是我们今天的重点 我们就跳过接下来,coflags跟costack size这两个东西都是在python的runtime里面Virtual Machine需要的一些数据stack size比较好理解对不对就是它需要的站的空间有多大那flags实际上是一个bitmap啊 我们看一下它的定义那flex在这里啊基本上就是他在编译的时候去判断一下这个code有没有什么特别的属性比如说有没有新args 有没有新型keyword args或者说它是不是一个generator 是不是一个coroutine呢通过这些flags呢python在运行这段代码的时候呢有可能会有不同的behavior比如他在运行一个generator也就是一个生成器函数的时候和他运行一个普通函数的时候那他肯定就是不一样那这个部分呢以后我们再介绍一些更硬核的东西的时候呢可能会接触到好 接下来这个部分就比较重要了啊是关于输入参数的数量的那这些属性呢决定了这个Python在往里面传参数的时候怎么处理这些参数他们也是python进行函数重载的一个基础大家知道 Python 的重载机制其实是非常非常灵活的我们在未来有机会的话会单独出一期视频来说这个函数调用的时候这个参数到底是怎么传的但是今天我们只把这三个count搞明白好 那我把官方文档上面对这三个看出来的介绍给粘到这个comment里了啊大家看一下应该就能感受到如果我们想理解这三个东西的话首先要知道什么叫positional only arguments什么叫keyword only arguments那我们大部分人在写python的时候呢argument无非就是用这么几种第一个是必须要输入的 就像A 对不对第二个是有一个 default value 的比如说 b那么第三个就是星args或者是星星 keyword args后面两个星ARX跟星星KeywordARX我们一般管它叫做变长参数就是你传进来多少我都接着但是这四种形式在F里面他们都 [5:00] 既不是position only,也不是keyword only不信的话我们可以跑一下这个程序好大家可以看到arguments数量是两个因为他这里面说了我要排除掉星和星星的argsPosition only跟Keyword only都是零的好 问题来了 什么叫做Position Only Arguments好 我们来看这段代码啊这个斜杠可能很多人比较陌生这个就是python里面Positional only argument的一个语法那在函数调用里positional arguments就是你直接给的一个数比如说F1这个1是positional的F1这两个1都是positional的那keyword argument就是你给进来的是一个keyword和一个valuea=1这里面keyword是a value是1这个叫keyword argument在python中进行函数调用的时候你要么是positional的要么就是keyword了那positional only顾名实义就是这两个变量必须得是positional的在F的definition里面 斜杠表示斜杠之前的所有变量必须要是positional传进来的我们看下面的三个例子F1是positional的传给A对吧然后B有一个默认值是3所以这个调用是合法的F11那么第1个E传给了A第二个E传给了B也是合法的但是我们看这个FA等于1由于这个F的定义里面 A和B都是Petitional only的所以这个A等于1并没有办法把1复值给A所以这个调用是非法的我们运行一下大家可以看到这个FA等于1报错了说是少了一个required的position argument同时我们看向上面 看向这个这一个值是我们打印的positional only argument count 现在是两个也就是在这个函数里面有两个positional only argument分别是A和B如果我们把函数钉里面这个斜杠拿掉我们可以看到就一切正常了同时position only argument count变成0了知道了什么叫position only之后什么叫keyword only argument我们看我们又改了一下函数的definition啊现在A逗号后面有一个什么都没有的新号了那这个新号就表示这个新号之后所有的参数都是keyword only argument依然是下面的三个钓用F1没有问题吧1复制给A但是F11之前的话是一个给A一个给B现在不行了因为必必须要拿到一个keyword argument我们运行一下试一下大家可以看到第三个打印出来的东西也就是keyword only argument count是一个同时FEE fail了因为他只能接受一个positional argument那我们怎么修复这个程序呢把这个F11变成F1B等于1然后我们再运行它就成长了好了 再了解了什么叫position only argument 什么叫keyword only argument之后我们再回头看这三个东西就比较好理解了Home两个分别就是Position-only argument和Keyword-only argument的数量第一个co-art count就是除了心儿和星星还有keyword only argument之外的所有的参数的数量那这个支持点呢 我们以后讲到这个函数重载的时候 可能会用得到 可能会用得到好那接下来就到了最复杂最容易混淆这个世界上都没有几个人搞得明白的这一大串names里面了在讲这些属性分别是什么意思之前我们要先思考一个问题就是bytecode这个东西应该怎么设计举个简单的例子我们一个函数argument是a然后你里面写了一行代码b等于a这个时候你应该运行一个什么样的字节码呢你的字节码里面应该包含A吗如果你觉得你的字节毛里面应该包含变量名的话那如果这个变量名非常非常非常长然后用了非常多次你的字节毛岂不是会非常长更重要的是 A 也好 B 也好他们在 runtime 的时候表示的是一个python object 对不对那如果你的字节目里面保存的是A或者B的话你怎么从这个A或者B再转到这个python object呢你肯定还要用一个mapping对不对你可能自己再建一个dictionary或者什么样的hash map这么一想你的效率是不是变得很低了无论在时间上还是空间上 这都是一个非常不好的设计那 Python 的设计是什么样的呢他首先认定了一个 [10:00] 就是在我编译代码的时候这个代码里面出现的变量名也好或者是任何的名也好他是有数的也就是说它完全可以做到一个OE的E对应那简单来说呢 就是我维护一个数族 我每见到一个名字我就给他指定一个交标比如说我见到A了 我说好A你就是0 对吧 我见到B说B你是1 哎 看来就可以搞对线 没有啊那通过这种方式呢 我的bycode里面就可以写 对吧 读取第0个位置的数据 保存到第1个位置里 这样无论从时间上还是从空间上都会有非常大的提升当然有的时候我们确实需要位置和变量名之间的对应关系怎么办呢我们就同样的维护名字的一个数组就是把我们见到的名字们A啊B啊也维护到一个数族里面保证顺序不变这样我们就可以通过index而不是一些复杂的mapping找到他们之间的关系好 讲了这么多理论 下面我们来说一下这几个东西分别是什么意思第一个概念叫做local variable就是局部变量Local variable包括了函数进来的所有的参数也就是arguments以及只在这个函数scope里面用到的变量那在我们现在的这个例子里呢显然B和A都是局部变量所有的local variable的名字包括argument的名字都被保存到covar names 里而co-nlocals是局部变量的数量在绝大多数情况下它就是coburnames的size那么python的原代码呢是对他们两个的size进行过检查但是我现在也没搞明白什么情况下它俩会不一样好 我们运行一下这个代码可以看到Nlocal是2对不对有两个local variable然后他们的名字分别是A和B那为了更好的讲解我刚才说的一些跟字节码有关的东西呢我们在尝试把它的字节码调出来好 我们看我们用this这个module啊 对f做了一个this点this 我们运行一下看一下这里的实际上字节码是loadfast0然后store faste后面那个括号的部分呢是this这个module帮我们写上去的在实际的字节版里只有loadfast00这个位置对应的是什么呢是varnames里面第0个index这里的名字也就是a刚才发现了笔物稍微改了一下我们就不同路了那var names是一个相对来说比较容易理解的概念就是local variable对不对这个Names是什么?如果大家还有印象的话我们刚才讲过一个codename那个cone name是这个函数的名字Codenames和Codename没有一毛钱的关系这里这个名字的设计稍微有点愚蠢它没有任何关系啊这个names保存的就是除了varnames除了sell wars跟free wars thrive剩下的所有不需要的name都扔到这个conames里比如说当我们尝试access一个attribute对吧 B等于A点ATTR 我们想一下这个ATTR存在哪了呢就存在这个Codenames里我们来看一下大家可以看到这个names里面有一个attr对不对然后这里有一个load htr0实际上它是把attr这个string给漏进了stack里那包括比如说method 对不对你一个class的method名字也是放在这里的我们可以看一下可以看Names现在是attr和method然后这个load method是1对不对?1这个index代表的是method这个string你可能比较容易想到这个variable的名字本身被存在一个地方但实际上在python程序里面出现的所有不是reserved word的这些stream都会被存进这些存名字的地方甚至比如说你做一个import我们看我们import了一个math对不对然后math就出现在了var names里和names里它出现在这两个位置的意义是不一样的在names里是因为他import map然后Math这个string需要保存起来而在 var names 里是因为它这个地方隐含着 import math 之后把import进来这个东西这个酷又保存给了math这个variable如果把这行代码改成 import math sm那么math就只出现在names里而m出现在varnames里math是要用到的String,而m是那个Variable好 接下来我们讲sellers跟freeers这两个概念有点像硬币的两面它们经常成对出现一般来说都是辅助完成臂包的cellvars的意思是我这个地方的variable还会在其他的 [15:00] 这叫Selvars而freevars是这个variable是从其他的scope来的那在python中的实现必包的时候呢实际上它是把这个object的变成了一个叫做cell object你可以把它理解为一个指针或者一个引用把它封装起来然后不同的scope可以同时access它这就是这个cellverse的cell的由来那币包的部分呢今天我们也没有时间讲啊所以只能未来再说我们看一下这个非常非常简单的函数啊这里面定义了一个d 一个dictionary对不对然后这里面的local函数f改变了D 然后把F return回来我们先看G的code object谢谢大家可以看到G的CellVars里面有一个D对不对就说明这个D不光被G本身locally使用他还在其他的scope使用了也就是F里面使用了如果if里面没使用的话它就会是一个var names 而不是一个cell vars我们可以试一下我们看如果F里面没有使用D的话那D就是一个G的Local variable好那我们接下来看一下f的code object啊因为我们g 函数return f所以我们只要调用g 得到就是f可以看到F里面有一个Free Verse是D因为D这个variable是别的scope它使用的所以是一个free worth那sell verse跟free verse存在的意义都是告诉这个函数你在给这个值进行复制或者读取的时候你要进行一些特殊的操作我们看还是this.this一下g如果这个F里面没有用到D的话那么这里他用的是storefast storefast也就是对varnames的操作好 那如果F用了D呢?我们看如果F用了D的话在G里面他用的就是StoreD Ref了这是一个专门针对笔包的bytecode下面还有load closure他从名字里面也能看出来他是跟币包有关的一个操作下面在F里面使用D的时候这里面也用的是loadDref所以我们就可以知道在我们看来长的一样的函数在python compile的时候他自己会去分辨这个函数里面是不是涉及了臂包这个variable是从哪来的从而去调整它的Bitcode有关臂包真正的实现方式我们未来有机会再单独做视频好 那最后一个是Const啊Const跟上面我们介绍的这四个保存String也就是保存名字的属性都不一样Const保存的是Python object你在这个函数里面所有出现的长量值都会保存在call counts里比如说我们现在F定义A等于1B等于ABCABC对不对那么这个1跟ABCABC都会保存在这个Cons里我们看一下这个Cons里面有None1跟ABC ABC那这个东西是常驻嘉宾的 它永远会在抗次力然后我们注意这里的1是一个Integer对不对ABCABC是一个Stream那这些内容被保存在Cons里面然后Python虚拟器在运行这些字节码的时候会从Cons里面调用这些数据好 那我们就把code object每一个属性都讲完了啊这期视频可能确实难度偏大 需要你理解很多底层的一些概念但是如果你真的想弄懂python底层的很多运行机制的话呢你躲不开这个code object所以还是希望这个全网唯一的介绍CodeObject的视频能对你有所帮助好 那这期视频就到这里 如果你喜欢的话别忘了点一下关注 记得一键三零哦字幕志愿者 杨茜茜