ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

CPython 对象模型底层拆解:PyObject 与 PyTypeObject 如何撑起 Python 的动态世界

2026/8/29 12:13:28 拓冰建站 浏览量
CPython 对象模型底层拆解:PyObject 与 PyTypeObject 如何撑起 Python 的动态世界 CPython 对象模型底层拆解PyObject 与 PyTypeObject 如何撑起 Python 的动态世界【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython在 CPython 里一个变量名和它指向的值之间没有任何绑定关系x 1之后再写x helloPython 甚至允许这种反复横跳。支撑这一切的是 CPython 对象模型里那块只有两个字段的基础结构体PyObject以及围绕它运转的引用计数和类型系统。本文带你把这套机制拆到字段级别。一、每个对象都随身携带的两样行李ob_refcnt 与 ob_type这一节回答一个问题不管对象多大、多复杂CPython 保证每个对象头部都有什么。所有 Python 对象在内存里的第一块字节都是同一个结构。定义在源码文件 Include/object.h这段代码要看的就是那个最小行李包的全部家当struct _object { Py_ssize_t ob_refcnt; // 引用计数当前有几个引用指向这个对象 PyTypeObject *ob_type; // 类型指针这个对象属于哪个图纸 };用机场安检做类比ob_type是通行证安检口任何接收PyObject*的 C 函数拿到它就知道该怎么放行——int走整数的通道str走字符串的通道自定义类的实例走实例的通道。ob_refcnt则是行李吊牌上的计数牌上数字归零行李就可以销毁。关键结论因为每个具体类型PyListObject、PyLongObject等都以这个结构体作为前缀字段所以任何对象指针都能向上强转成PyObject*通用处理。这是 CPython 一切皆对象在 C 层面的真实含义。这两样行李决定了 CPython 内存管理的基调没有统一的堆管理器替你记账记账本就缝在每个对象自己身上释放时机因此可以做到精确到毫秒级也就是引用归零的那一刻。二、类型是怎么从基类零件拼装出来的字符串、列表、整数这一节展示基类 专属零件的拼装手法并给出三种数据类型的结构对比。CPython 实现类型扩展的方式不是 C 继承而是结构体嵌套把PyObject作为第一个字段嵌进新结构体后面跟自己的零件。效果类似乐高积木——底座是通用的上层按需拼。顺序上我们从最复杂的讲起。字符串str的内部结构定义在 Include/unicodeobject.h里除了PyObject_HEAD即上面的两个字段加变长标记的宏展开和ob_size还有缓存的hash值、驻留intern把相同内容的字符串指向同一份内存以省空间状态、编码宽度kind按内容自适应 UCS-1/2/4等标志位。字符串是零件最多的典型为了加速len()、哈希和驻留它把大量状态直接摊平在对象头里。列表listInclude/listobject.h的专属零件很克制只有指针数组和已分配容量typedef struct { PyObject_VAR_HEAD // PyObject ob_size当前元素数 PyObject **ob_item; // 元素指针数组 Py_ssize_t allocated; // 已分配容量通常大于 ob_size } PyListObject;allocated就是 list 预留空间over-allocation的来源空间不够时按策略扩容所以append()摊还下来是 O(1)。整数Include/longobject.h则是变长数组的另一种形态固定头部 一个digit数组每个digit存 30 位64 位平台符号存在标签位里头部本身不额外占正负号字段。三者差异一张表看对象类型变长部分专属字段设计动机str字符数据hash、intern/kind 等标志位加速长度、哈希与驻留按内容选最省编码list元素指针数组ob_item、allocated预留容量让append()摊还 O(1)intdigit数组每 30 位一段标签位符号 位数大整数按需扩容小整数可复用避坑提示Py_SIZE()/ob_size对定长对象如int的标量用法无意义写 C 扩展前确认你的类型到底是变长还是定长别想当然。三、一个对象的生老病死Python 引用计数全流程这一节把ob_refcnt的动态过程走一遍并标出增减的时机。 对象的一生可以压缩成四个动作出生计数置 1、交接新引用 1、松手旧引用 -1、死亡归零即销毁。用一张时序图看下来最清楚配套 API 就三个Py_INCREF加一、Py_DECREF减一归零时立即触发类型对象里的tp_dealloc、Py_REFCNT读当前值。注意引用计数管不到循环引用A 引用 B、B 又引用 A两者计数都到不了 0这类残留交给带Py_TPFLAGS_HAVE_GC标志的类型、由循环垃圾回收器兜底——引用计数是主引擎GC 是补胎工具。四、行为合同PyTypeObject 与 tp_ 方法指针表这一节看通行证背面印了什么对象能做什么由谁说了算。ob_type指向的PyTypeObject完整定义同样在 Include/object.h可以理解为一份产品说明书加合同条款tp_name是型号名tp_basicsize是单体尺寸剩下是一排方法指针槽位——tp_dealloc管怎么拆tp_hash管能不能当字典键tp_call管能不能被()调用tp_iter/tp_iternext管迭代。CPython 里对象能做 X这句话的完整含义就是合同里对应那一格填了函数而不是空格。typedef struct _typeobject { PyObject_VAR_HEAD const char *tp_name; // 类型名如 int Py_ssize_t tp_basicsize; // 实例基础大小 destructor tp_dealloc; // 析构对象死亡时的清理动作 hashfunc tp_hash; // 哈希决定能否做 dict 的键 ternaryfunc tp_call; // 调用被 () 调用时执行什么 // ... 还有迭代、比较、属性等数十个槽位 } PyTypeObject;tp_flags是合同末尾的勾选框Py_TPFLAGS_HAVE_GC表示这类型参与循环回收Py_TPFLAGS_IMMUTABLETYPE表示类型本身不可再改行为。解释器执行5 3时就是沿着5这个对象的ob_type找到int的合同再从合同里取出加法对应的函数执行——动态分派方法查找在运行时按类型发生的全链路到此闭环。五、自己动手验证sys.getrefcount 与class两行验证这一节给你两段最小代码亲手摸到上面讲的两个字段。第一段验证引用计数注意getrefcount会把形参本身算进去读数比裸计数多 1import sys x 1000000 # 大整数不会被小整数缓存复用 print(sys.getrefcount(x)) # 2变量 x 函数形参 y x print(sys.getrefcount(x)) # 3 del y print(sys.getrefcount(x)) # 2第二段验证ob_type这条线索__class__在 C 层读的就是ob_type指向的PyTypeObjects hello print(s.__class__) # class str print(s.__class__.__hash__) # 类型对象自己也能被哈希跑完这两段第二部分的通行证和第三部分的计数牌就都从抽象变成了你能读出的数字。六、三条能带走的设计经验这一节把对象模型里可以搬去自己项目的设计决策提炼出来。极简核心PyObject只有引用计数和类型指针两个字段所有动态特性都压在类型指针背后——基础接口越小性能和维护成本越低。组合优于继承CPython 不用语言级继承而是把基类结构体作为第一个字段嵌套进每个具体类型向上强转能力由此白得新类型无需改动任何既有代码。开闭原则新行为通过PyTypeObject的方法指针表注入扩展是往空槽里填函数核心执行路径ceval 与PyObject对新增类型保持关闭。延伸阅读想动手翻源码的话按这个顺序效率最高PyObject与PyTypeObject定义Include/object.h变长对象头PyVarObject、PyObject_HEAD宏Include/object.h前半部分列表对象结构Include/listobject.h整数对象与大数表示Include/longobject.h字符串对象结构Include/unicodeobject.h结语留一个问题给你既然引用计数如此精确为什么 CPython 还要额外维护一个基于可达性分析的循环垃圾回收器两个机制各自兜住了对方兜不住的哪类对象想清楚这一点你就真正理解 CPython 内存管理的双层设计了。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考