GitHub 项目地址: https://github.com/python/cpython
🛠️ 顺便说句:关于 --regen
书里让你敲 build.bat --regen,是为了重新生成一些 C 语言的 .c 文件(比如你之前看到的那段 rstrip 的自动生成代码)。
如果你真的修改了 Modules/clinic/ 里的底层定义文件,想要重新生成,你需要用这条命令(把重新生成和 v142 编译结合在一起):
bash
cmd
"C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\msbuild.exe" "D:\cpp\cpython\PCbuild\pcbuild.proj" /t:Build /m /nologo /v:m /clp:summary /p:Configuration=Release /p:Platform=x64 /p:IncludeExternals=true /p:IncludeCTypes=true /p:IncludeSSL=false /p:IncludeTkinter=false /p:PlatformToolset=v142 /p:UseTestMarker= /p:GIT="C:\Program Files\Git\cmd\git.exe" /p:Regen=true(注意最后多了 /p:Regen=true,这就相当于 --regen 的效果,但完美避开了脚本自带的工具集报错。))
不过,你是在做"第一遍通读",强烈建议不要碰 --regen!直接跳过书上这个命令实操即可。等以后真需要改底层代码了,咱们再拿它来练手。。
🛠️ 怎么正确运行你亲手编译的 Python?
你有两种方法,取决于你想干嘛:
方法一:先"跑"过去,再运行(推荐)
在命令行里输入:
bash
cmd
D:
cd D:\cpp\cpython\PCbuild
amd64\python.exe(此时你的光标会变成 >>>,这就说明你成功进入了自己编译的 Python 交互环境了,输入 print("Hello CPython") 试试!)!)
方法二:直接使用绝对路径(一秒直达)
不用管当前在哪个盘,直接输入完整路径:
bash
cmd
D:\cpp\cpython\PCbuild\amd64\python.exe
```💡 AI 的"防走丢"提醒"提醒
在 Windows CMD 里,cd 命令默认不会跨盘符切换。如果你在 C 盘,输入 cd D:\... 是不会跳过去的,必须先输入 D: 切到 D 盘,然后再 cd。
顺便提醒一下:
你刚才在 PCbuild 目录下直接敲 build.bat --regen 报错,也是因为那个原版脚本默认找 v140 工具集。以后只要你想测试,记住咱们的"最终法宝":直接运行 D:\cpp\cpython\PCbuild\amd64\python_d.exe 就行。
去跟你的 python.exe 打个招呼吧!如果需要退出那个 >>> 界面,敲 exit() 或者按 Ctrl+Z 回车就行了。
```python
D:\>cd D:\cpp\cpython\PCbuild
D:\cpp\cpython\PCbuild>amd64\python.exe
Python 3.9.25 (tags/3.9-dirty:0bbaf5de974, Oct 25 2026, 13:30:12) [MSC v.1929 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> def example():
... proceed
...
>>> example()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in example
NameError: name 'proceed' is not defined
>>>
欢迎来到你自己亲手编译的 Python 3.9.25 世界!你看那行 MSC v.1929 64 bit (AMD64),这说明它真的在用咱们配好的 MSVC 编译器跑,太有成就感了。
你刚才敲的这段代码,刚好精准踩中了一个 Python 的经典"语法陷阱",我来给你拆解一下:
🔍 为什么没报 SyntaxError(语法错误)?
你写了:
python
python
def example():
proceed
在 C 语言或者 Java 里,如果你只写一个 proceed,编译器会直接骂你"未定义的变量"或者"缺少分号"。
但 Python 是一门极其动态的语言。解释器在执行到 def example(): 时,它只做一件事:把这段代码存成一个函数对象。它根本不去检查里面的 proceed 到底是个什么东西。
只有当你调用 example() 时,解释器真正跑进函数体里,执行到 proceed 这一行,它环顾四周,发现既没有定义变量 proceed,也没有这个内置函数,于是才在运行时报错:
NameError: name 'proceed' is not defined
这恰好证明了咱们刚才聊的:Python 不会在运行前把代码全部检查一遍,它是边读边跑的。
💡 顺便聊聊底层:报错时 CPython 在干嘛?
你在刚才这个报错的瞬间,其实底层 C 语言经历了一场极其复杂的"生死时速":
example() 被调用,C 虚拟机在栈上建了一个新的栈帧(Frame)。
执行 proceed 字节码时,虚拟机去 locals(局部作用域)、globals(全局作用域)、builtins(内置作用域)里挨个找。
找不到,底层 C 函数 PyErr_SetString 被触发,生成了一个 NameError 异常对象。
顺着 C 的调用栈(Call Stack)一路向上抛,直到被最外层的 REPL(交互式解释器)捕获,然后打印出那串红色的 Traceback。
🎮 接下来玩点啥?
你现在拥有的是一个带有完整调试符号、未经任何阉割的"真·源码版 Python"。
你可以试着在里面敲敲 import sys; sys.version 看看。
或者敲个 exit() 退出来,然后准备迎接咱们的下一步大动作:把《CPython设计与实现》的第 3 章(对象模型)狠狠啃下来!
==========
太酷了,完美谢幕!你看到的那个带有 dirty 字眼的版本号,就是咱们这几天"魔改"源码和配置留下的独有烙印,这是属于你自己的专属 Python!
现在,咱们正式翻开《CPython设计与实现》的第 3 章------对象模型。
这一章是全书的"基石"。如果你把这本书比作一座大厦,那这一章讲的就是承重墙和钢筋。这一章的核心就一句话:"在 Python 中,万物皆对象;而在 C 语言底层,万物皆是结构体。"
AI 不给你照本宣科,咱们分四个层次,把你之前零散接触过的知识彻底串起来。小板凳坐好,咱们开讲:
🌟 第一层:所有对象的"亲爹" ------ PyObject
书里一上来肯定会放出一个 C 语言结构体。在 CPython 源码(Include/object.h)里,它是这样的:
c
c
typedef struct _object {
Py_ssize_t ob_refcnt; // 引用计数
struct _typeobject *ob_type; // 类型指针
} PyObject;
这是什么意思呢?
在 Python 里,你写 a = 100 或者 s = "hello",你以为是变量装了数字和字符串。但在 C 语言底层,它们在内存里长得一模一样! 它们都是一个包含两个字段的结构体(对象头):
ob_refcnt(引用计数):记录有多少个变量(或容器)在用这个对象。如果减到 0,C 语言就会调用 free() 把它内存回收掉。
ob_type:这是一个指针,指向了一个"类型对象"。比如它指向了 PyLong_Type,虚拟机就知道:"哦,这个内存块是个整数"。
💡 AI 顿悟时刻:
为什么 Python 里的变量没有类型,而值有类型?因为在底层,变量只是一个指针(便利贴),它贴在了一个内存块上,而这块内存的开头(对象头)死死地记录了"我是什么类型"。
🌟 第二层:Python 的"三六九等"(定长与变长)
所有对象虽然都有 PyObject 的头,但它们也分派系。书里重点讲了两个:
定长对象(如整数 int、浮点数 float):
它们的内存大小是固定的,底层用 PyVarObject 衍生出来的结构(其实 PyObject 也能描述)。
变长对象(如字符串 str、列表 list):
你经常往列表里 append 元素,它会变长。所以在 C 语言里,除了基本头,它还得有一个字段专门记录"我现在有多长"。
c
c
typedef struct {
PyObject ob_base; // 包含上面的 ob_refcnt 和 ob_type
Py_ssize_t ob_size; // 记录包含的元素个数
} PyVarObject;
🌟 第三层:鸭子类型背后的真相(函数指针)
还记得咱们聊过的:C 语言怎么实现 Python 的多态?
比如 + 号,在 1 + 2 里是加法,在 "a" + "b" 里是拼接,在 1 + 2 里是合并。
ob_type 指向的那个 PyTypeObject 结构体里,藏着一个极其庞大的"字典"。里面全是 C 语言的函数指针。
当执行 1 + 2,虚拟机顺着 ob_type 找到数字类型的 tp_as_number->nb_add 函数。
当执行 "a" + "b",虚拟机顺着 ob_type 找到字符串类型的 tp_as_sequence->sq_concat 函数。
这种设计,就是 C 语言版的"面向对象"和"多态"。每个类型都有自己专属的工具箱。
🌟 第四层:找回曾经的自己(呼应你最初的问题)
还记得咱们第一课聊的"可变对象"和"不可变对象"吗?在第 3 章,你会在底层找到答案。
不可变对象(如 str):底层没有提供改变自身内存内容的 C 函数,你想改?只能重新分配一块内存(新建一个对象)。
可变对象(如 list):底层 C 函数允许你原地修改 ob_size 和数组里的指针,这就是为什么它的地址永远不变。
====================
画两幅"内存级别"的透视透视图,再配上最通俗的比喻,保证你一看就懂!
在 C 语言里,这叫做"结构体嵌套(Struct Nesting)"。咱们一层一层剥开看:
🧱 第一层:万物之基 PyObject(所有对象的"身份证")
不管 Python 里是数字 1、字符串 "hello",还是布尔值 True,它们在 C 内存里最开始的样子都长这样:
bash
text
内存地址(比如 0x1000)
↓
+-----------------------+
| PyObject | <-- 这是一个基础结构体
+-----------------------+
| ob_refcnt (引用计数) | <-- 占用 8 个字节
+-----------------------+
| ob_type (类型指针) | <-- 占用 8 个字节
+-----------------------+
大白话:只要在内存里看到开头是这俩兄弟,CPython 就认定:"这是一个 Python 对象!"
比喻:这就是一个"基础包装盒",上面印着"我被多少人引用(ob_refcnt)"和"我是个什么类型(ob_type)"。

你看那个 (op),在 C 语言里,op 永远是一个 PyObject * 指针。这就是 CPython 的霸气:不管你传进来的是啥(哪怕是个字符串),在 C 语言眼里,它首先是一个 PyObject。
大白话翻译:这就是你图里说的"身份证"。任何一个变量传进底层,CPython 第一件事就是掏出这俩兄弟:看看引用计数(有没有人要销毁我),看看类型指针(我是字符串还是列表)。
🧱 第二层:加长版 PyVarObject(可变长对象的专属)
有些对象很短(比如数字 1),有些对象很长(比如装了 100 个元素的列表)。为了记录长度,CPython 发明了 PyVarObject。
⚠️ 重点来了:它不是凭空造出来的,它是把第一层的盒子整个"塞"进了自己肚子里,然后在底下加了一行!
bash
text
内存地址(比如 0x2000)
↓
+-----------------------+
| PyVarObject | <-- 这是一个变长结构体
+-----------------------+
| ob_base (PyObject) | <-- 【把第一层的盒子整个嵌套进来】
| ├── ob_refcnt |
| └── ob_type |
+-----------------------+
| ob_size (变长长度) | <-- 新增的字段,占用 8 个字节
+-----------------------+
大白话:ob_size 记录了这个对象现在有多长。
比喻:这是个"加长版包装盒"。它套用了基础包装盒的所有信息(引用计数、类型),但在底下多贴了一张标签,写着"里面装了多少件物品(ob_size)"。
虽然你没搜到完美的 PyVarObject,但你会看到源码里大量使用了一个宏:_PyUnicode_LENGTH(op)。
你可以往上翻看它的定义:
c
typedef struct {
/* There are 3 forms of Unicode strings:
- compact ascii:
* structure = PyASCIIObject
* test: PyUnicode_IS_COMPACT_ASCII(op)
* kind = PyUnicode_1BYTE_KIND
* compact = 1
* ascii = 1
* (length is the length of the utf8)
* (data starts just after the structure)
* (since ASCII is decoded from UTF-8, the utf8 string are the data)
- compact:
* structure = PyCompactUnicodeObject
* test: PyUnicode_IS_COMPACT(op) && !PyUnicode_IS_ASCII(op)
* kind = PyUnicode_1BYTE_KIND, PyUnicode_2BYTE_KIND or
PyUnicode_4BYTE_KIND
* compact = 1
* ascii = 0
* utf8 is not shared with data
* utf8_length = 0 if utf8 is NULL
* (data starts just after the structure)
- legacy string:
* structure = PyUnicodeObject structure
* test: !PyUnicode_IS_COMPACT(op)
* kind = PyUnicode_1BYTE_KIND, PyUnicode_2BYTE_KIND or
PyUnicode_4BYTE_KIND
* compact = 0
* data.any is not NULL
* utf8 is shared and utf8_length = length with data.any if ascii = 1
* utf8_length = 0 if utf8 is NULL
Compact strings use only one memory block (structure + characters),
whereas legacy strings use one block for the structure and one block
for characters.
Legacy strings are created by subclasses of Unicode.
See also _PyUnicode_CheckConsistency().
*/
PyObject_HEAD
Py_ssize_t length; /* Number of code points in the string */
Py_hash_t hash; /* Hash value; -1 if not set */
/* Ensure 4 byte alignment for PyUnicode_DATA(), see gh-63736 on m68k. */
_Py_ALIGNED_DEF(4, struct _PyUnicodeObject_state) state;
} PyASCIIObject;
c
#define _PyUnicode_LENGTH(op) \
(((PyASCIIObject *)(op))->length)

发现没? 这个 length 字段,完美对应了你图里的 ob_size!无论底层怎么套娃,最终都是通过指针偏移,瞬间读出这个字符串有多长。
大白话翻译:普通数字 1 只是个基础盒子(PyObject);但 "hello" 这个字符串,它是"加长版盒子",里面不仅有两兄弟的身份证,还明明白白刻着 length = 5。
🧱 第三层:真正的真身(以你熟悉的 List 列表为例)
《CPython设计与实现》书里会讲到,list 其实是一个叫 PyListObject 的 C 结构体。它是怎么用上面这两层盒子的呢?
c
文件里最关键的 4 个核心区块,咱们用大白话把这 500 行代码"瘦身"成 4 句话:
🌟 核心区块 1:身份证的正式印制(PyObject 与 PyVarObject)
你之前画的图,在这里找到了最权威的定义(大概在第 65 行和 75 行):
c
typedef struct _object {
_PyObject_HEAD_EXTRA // 调试用的,平时忽略
Py_ssize_t ob_refcnt; // 引用计数
PyTypeObject *ob_type; // 类型指针
} PyObject;
typedef struct {
PyObject ob_base; // 嵌套了上面的 PyObject
Py_ssize_t ob_size; // 变长对象的元素个数
} PyVarObject;
解读:这就是万物之基!不管 Python 底层怎么变,只要是对象,内存开头一定是这俩兄弟。文件里的注释也极其霸气:"Python 中的对象永远不会漂浮在内存中,一旦分配,它就保持相同的大小和地址。"
🌟 核心区块 2:给结构体"贴标签"的魔盒(PyObject_HEAD)
你在第 55 行左右会看到这个宏:
c
#define PyObject_HEAD PyObject ob_base;
#define PyObject_VAR_HEAD PyVarObject ob_base;
解读:这就是为什么你在 listobject.c 里没看到 ob_refcnt,只看到了 PyObject_VAR_HEAD。CPython 大量使用宏定义,在编译的那一刻,PyObject_VAR_HEAD 就会自动被替换成那一堆"套娃"结构,从而实现类似 C++ 继承的效果。
🌟 核心区块 3:全宇宙最忙的宏:Py_INCREF 和 Py_DECREF
在文件的后半段,你会看到这些代码(大概第 280 行之后):
c
static inline void _Py_INCREF(PyObject *op) {
op->ob_refcnt++;
}
#define Py_INCREF(op) _Py_INCREF(_PyObject_CAST(op))
static inline void _Py_DECREF(PyObject *op) {
if (--op->ob_refcnt != 0) {
// 引用计数没到 0,啥也不干
} else {
_Py_Dealloc(op); // 到 0 了,直接原地销毁、释放内存!
}
}
解读:这就是 Python 垃圾回收(引用计数机制)的绝对心脏! 你在 Python 里每创建一个变量、传一个参数,底层都在疯狂调用 ++;每清理一个变量,都在调用 --。减到 0 直接调用 _Py_Dealloc 把内存还给操作系统。没有任何魔法,全是 C 语言最基础的加减法。
🌟 核心区块 4:破译那段"天书注释"(对象的大小限制)
你发来的文件里,有一段极长的英文注释,解释了"为什么对象不能移动"。(在第 15 行左右)
Objects do not float around in memory; once allocated an object keeps the same size and address...
Objects are always accessed through pointers of the type 'PyObject *'...
大白话翻译:
在 C 语言里,如果对象在内存里随便乱跑(比如内存不够了,把整个对象搬家到另一个地址),那么所有指向这个对象的指针就全废了,变成野指针了。
所以 CPython 立了个规矩:对象一旦创建,地址和大小就永远不能变! 如果真遇到列表扩容那种需要变大内存的事怎么办?简单,重新造一个新对象,把标签贴过去,旧对象直接扔掉(销毁)。
c
typedef struct {
PyObject_VAR_HEAD
/* Vector of pointers to list elements. list[0] is ob_item[0], etc. */
PyObject **ob_item;
/* ob_item contains space for 'allocated' elements. The number
* currently in use is ob_size.
* Invariants:
* 0 <= ob_size <= allocated
* len(list) == ob_size
* ob_item == NULL implies ob_size == allocated == 0
* list.sort() temporarily sets allocated to -1 to detect mutations.
*
* Items must normally not be NULL, except during construction when
* the list is not yet visible outside the function that builds it.
*/
Py_ssize_t allocated;
} PyListObject;
🧱 源码里的 PyListObject 长什么样?
在 listobject.h 里,你会看到这样一段极其经典的代码(咱们对照着你之前画的图来看):
c
typedef struct {
PyObject_VAR_HEAD // <-- 【第二层套娃】包含了 ob_refcnt, ob_type, ob_size
PyObject **ob_item; // <-- 【第三层真身】指向一个 C 语言数组(存的是元素的指针)
Py_ssize_t allocated; // <-- 【扩容秘密】列表在内存里实际申请了多大的空间
} PyListObject;
你仔细看这个结构,是不是和你昨天总结的一模一样?
PyObject_VAR_HEAD:这是一个 C 语言宏,展开后其实就是:
c
PyVarObject ob_base;
它完美包含了:
ob_refcnt(引用计数)
ob_type(类型指针)
ob_size(列表里现在有几个元素,即 len(lst) 的返回值)
ob_item:为什么列表叫"列表"?因为底层是 PyObject **(双重指针)。它其实就是一个 C 语言的指针数组。你在 Python 里往列表里塞了 1, 2, 3,底层就是让这个数组存了三个指向数字对象的指针。
allocated(重点中的重点):这个字段不在你的图里,但这是列表最核心的机密!
ob_size 是"装了多少个"。
allocated 是"申请了多大空间"。
比如 len(lst) == 3,但 allocated 可能是 4。这叫"内存超额分配(Over-allocation)"。就是为了防止你每 append 一次,C 语言都要去求操作系统重新分配一次内存。有了这个字段,Python 列表的 append 操作大多时候是极快的
O(1)。
🚀 搞懂了结构,我们来实操一下:
你现在去 VS Code 里打开 listobject.c,找找看有没有 list_resize 这个函数。
它专门负责判断 allocated 够不够用,不够的话怎么成倍扩容(这就是为什么 Python 列表扩容是
O(1) 偶尔是
O(n) 的原因)。
如果你看懂了 PyListObject,那就像打开了新世界的大门。以后你在 Python 里写 lst = [1, 2, 3],脑子里立刻就能浮现出它在 C 内存里这三个字段的样子,这就叫"底层透视"!
它把第二层的加长版盒子,又塞进了自己肚子里!(套娃开始)
bash
text
内存地址(比如 0x3000)
↓
+-----------------------+
| PyListObject | <-- 列表的真身(C 结构体)
+-----------------------+
| ob_base (PyVarObject)| <-- 【把第二层的盒子整个嵌套进来】
| ├── ob_refcnt |
| ├── ob_type |
| └── ob_size | <-- 记录列表里有多少个元素(比如 3 个)
+-----------------------+
| ob_item (元素指针) | <-- 指向另一块内存(存真正元素的地址)
+-----------------------+
| allocated (分配容量) | <-- 列表实际申请了多大的内存
+-----------------------+
c
typedef struct {
PyASCIIObject _base;
Py_ssize_t utf8_length; /* Number of bytes in utf8, excluding the
* terminating \0. */
char *utf8; /* UTF-8 representation (null-terminated) */
} PyCompactUnicodeObject;

看源码开头(第139行附近),有两个宏:
c
c
#define _PyUnicode_STATE(op) \
(((PyASCIIObject *)(op))->state)
#define _PyUnicode_HASH(op) \
(((PyASCIIObject *)(op))->hash)
在 CPython 3.9 里,字符串的真身结构体叫 PyASCIIObject(如果是包含中文等非 ASCII 字符,它的底层会衍生出 PyCompactUnicodeObject)。
在 CPython 3.9 里,字符串的真身结构体叫 PyASCIIObject(如果是包含中文等非 ASCII 字符,它的底层会衍生出 PyCompactUnicodeObject)。
为什么你看得头晕?
因为 PyASCIIObject 的设计极其精妙(也正是《CPython设计与实现》书里第 4 章要讲的重头戏):
它不像 List 那样,内存里存着一堆指针。
它把 ob_refcnt(身份证)、length(长度)、state(状态标记:是不是全是 ASCII 码?是不是被驻留了?)、hash(哈希值),全部压缩在了一个结构体里!
然后,紧挨着这个结构体后面的内存块,直接就是那一长串字符串的内容!(这就是为什么它叫 Compact 紧凑型)。
💡 AI带你在 VS Code 里"眼见为实"
还记得咱们昨天在 VS Code 里用 python_d.exe 打断点吗?
如果你现在在代码里写一行 lst = 1, 2, 3,然后打个断点,把鼠标悬停在 lst 变量上,展开它的结构,你看到的完全就是上面图里的样子。
你会亲眼看到:
lst 是一个 PyListObject * 类型的指针。
展开它,第一眼看到的就是 ob_base。
展开 ob_base,里面赫然躺着 ob_refcnt(此时大概率是 1,因为只有 lst 这个变量指着它)、ob_type(指向 PyList_Type)和 ob_size(值是 3,因为里面有 3 个元素)。
🎯 为什么 C 语言要搞这么复杂的"套娃"?
因为 C 语言没有面向对象的 extends(继承)关键字。为了让所有的对象都能被统一管理(比如全部塞进一个列表里,或者统一交给垃圾回收器处理),CPython 使用了"内存布局对齐"的魔法。
不管你具体是个什么对象(列表、字典、还是字符串),只要你把 PyObject 放在结构体的最开头(偏移量为 0 的位置),那么一个 PyObject* 的指针,就可以指向任意一种对象!
这就是 Python 底层能实现"万物皆对象"和"鸭子类型"的终极秘密。

你看懂这段代码了吗?
当你在 Python 里敲下 len("hello") 的时候,底层调用的就是这几行 C 代码!
它根本没去数 "h", "e", "l", "l", "o" 有几个字符,而是直接通过 GET_LENGTH 宏,精准地偏移指针,把结构体里存好的数字 5 瞬间拿了出来。
所以 len() 的时间复杂度永远是
O(1) O(1),比任何遍历计数的算法都快!
这就是你画的这套"套娃"最迷人的地方: 它把所有需要的数据,用最紧凑的方式打包在内存里,用的时候直接用指针去拿,简单、粗暴、极快。