尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

a = 42 之后,PyObject 里发生了什么

a = 42 之后,PyObject 里发生了什么 a 42 之后PyObject 里发生了什么【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpythona 42执行完的那一刹那CPython 在堆上分到一块内存往里写了一个计数器和一个类型指针。CPython 对象模型靠struct _object的两个字段管理所有对象的生死再靠PyTypeObject调度它们的全部行为。读完你能带走三样东西PyObject 在内存里的真实布局、引用计数从 1 加到 0 的完整时序、以及类型表里每个方法指针各管什么。对象的身份证PyObject 结构体任何 Python 对象在 C 层面的起点都是同一个结构体它定义在 Include/object.h 中。下面这段就是当前源码里struct _object的原文64 位、GIL 构建下struct _object { _Py_ANONYMOUS union { #if SIZEOF_VOID_P 4 int64_t ob_refcnt_full; struct { uint32_t ob_refcnt; uint16_t ob_overflow; uint16_t ob_flags; }; #else Py_ssize_t ob_refcnt; #endif _Py_ALIGNED_DEF(_PyObject_MIN_ALIGNMENT, char) _aligner; }; PyTypeObject *ob_type; // part of stable ABI; do not change };逐字段看它只有两件东西ob_refcnt引用计数器。64 位构建下它不是独占一个 64 位字而是和ob_overflow计数溢出位、ob_flags对象标志位打包在同一个 64 位字里ob_refcnt_full则是这个完整字的别名。ob_type指向这个对象所属类型的PyTypeObject实例源码注释明确标注它属于 stable ABI不许改动。匿名 union 里的_aligner不存数据只保证对象至少满足_PyObject_MIN_ALIGNMENT的对齐要求——对齐错了后面的原子增减计数就会在部分架构上出 bug。两个字段就够CPython 的最小契约两个字段够用是因为它是什么随时可以用ob_type查表还有谁在引用它由ob_refcnt记账。解释器处理一个PyObject *指针时只做两件事用Py_TYPE(op)宏取出类型指针再跳到类型表里找对应行为用Py_REFCNT(op)宏读取计数器用Py_INCREF(op)/Py_DECREF(op)宏做加减。⚠️ 扩展代码里只用Py_INCREF/Py_DECREF宏操作计数手写ob_refcnt既绕过了溢出位维护也可能与原子操作产生竞争。这个最小契约的好处是任何新类型都只需在数据字段前嵌一个PyObject基结构永远不用改。对象的出生三种内存布局对象出生时先决定长什么样定长、变长、字符串各有自己的布局策略。定长对象int 如何嵌套 PyObject整数对象的结构比很多人印象里的更紧凑它定义在Include/cpython/longintrepr.h中typedef struct _PyLongValue { uintptr_t lv_tag; /* Number of digits, sign and flags */ digit ob_digit[1]; } _PyLongValue; struct _longobject { PyObject_HEAD _PyLongValue long_value; };PyObject_HEAD宏展开为PyObject ob_base也就是完整内嵌了一个struct _objectlv_tag把符号位和位数打包在自己的低位里所以新版长整数不再依赖ob_size字段ob_digit是数组源码里写 1 个元素实际按位数弹性展开每个digit存 30 bit。42只需 1 个 digit连同计数器一起装进一块小内存。变长对象list 的指针数组与 over-allocation列表结构定义在Include/cpython/listobject.h中typedef struct { PyObject_VAR_HEAD PyObject **ob_item; Py_ssize_t allocated; } PyListObject;PyObject_VAR_HEAD比PyObject_HEAD多一个ob_size来自PyVarObject记录当前元素个数ob_item是PyObject *指针数组存的是元素的引用而非元素本身allocated是已分配容量源码注释给出的不变量是0 ob_size allocated。allocated大于ob_size的余量就是 over-allocation 预分配list.append空间不够时才扩容摊下来每次追加是均摊 O(1)。字符串的多档存储UCS-1/2/4 与 ASCII 快速路径字符串按内容最宽代码点选最省的字符宽度核心结构同样内嵌PyObject_HEADtypedef struct { PyObject_HEAD Py_ssize_t length; Py_hash_t hash; _Py_ALIGNED_DEF(4, struct _PyUnicodeObject_state) state; } PyASCIIObject; struct _PyUnicodeObject_state { unsigned int interned:2; unsigned int kind:3; unsigned int compact:1; unsigned int ascii:1; unsigned int statically_allocated:1; unsigned int :24; };state位域里 5 个有名状态位各有分工位宽度含义interned2 bit0 未驻留 / 1 驻留 / 2 驻留且 immortal / 3 再加 statickind3 bitPyUnicode_1BYTE_KIND(1) /PyUnicode_2BYTE_KIND(2) /PyUnicode_4BYTE_KIND(4)决定每个码点占 1、2 还是 4 字节compact1 bit紧凑对象只用一块内存结构体 数据一次分配ascii1 bit纯 ASCIIU0000–U007Fstatically_allocated1 bit静态分配不参与释放纯 ASCII 字符串走PyASCIIObject快速路径数据直接紧跟结构体length同时就是 UTF-8 长度不需要额外缓冲含非 ASCII 字符的紧凑串则由PyCompactUnicodeObject追加utf8_length与utf8指针。对象的生存引用计数记账引用计数的本质是一本账每次多一个引用就记一笔收入少一个就记一笔支出账平到 0 才销户。INCREF / DECREF一次完整的计数时序步骤你的代码解释器做的事ob_refcnt1a 42分配_longobject写入ob_type PyLong_Type12b aPy_INCREF(a)23del bPy_DECREF(a)14a NonePy_DECREF(a)计数归 0调用ob_type里的tp_dealloc内存归还分配器0注意第 4 行归零不会只清一个数字tp_dealloc会递归处理对象内部持有的引用——列表要先对每个ob_item[i]逐个Py_DECREF再释放ob_item数组本身。亲手验证sys.getrefcount 的 1 偏移下面这段代码可以直接运行输出与注释一致import sys a [1, 2, 3] print(sys.getrefcount(a)) # 2 b a print(sys.getrefcount(a)) # 3 del b print(sys.getrefcount(a)) # 2第一次输出是 2 而不是 1本地变量a占 1 笔调用sys.getrefcount(a)时实参本身又构成一笔临时引用函数返回前这笔引用就消失。所以这个 API 的读数永远比你以为的引用数多 1做对比实验时两侧都要算上它。对象的行为档案卡PyTypeObject类型表是一张方法指针数组对象身上不写一行逻辑全靠ob_type指向的这张表决定行为。方法指针表tp_dealloc、tp_hash、tp_iter 各管什么从 Include/object.h 的PyTypeObject中摘出关键字段...处省略了完整定义中的其余成员typedef struct _typeobject { PyObject_VAR_HEAD const char *tp_name; Py_ssize_t tp_basicsize; Py_ssize_t tp_itemsize; destructor tp_dealloc; ... hashfunc tp_hash; ternaryfunc tp_call; ... getiterfunc tp_iter; iternextfunc tp_iternext; ... unsigned long tp_flags; ... } PyTypeObject;各指针的职责一目了然tp_name/tp_basicsize/tp_itemsize类型名与实例尺寸分配内存前先看这里tp_dealloc计数归零时的清理入口列表、字符串都在这里释放自己的数据缓冲tp_hashhash(x)的落点决定对象能否当字典键tp_call装了就支持x(...)调用tp_iter/tp_iternext迭代协议的两个钩子for循环靠它们逐步取值。 动态语言的多态就发生在这里解释器从不判断对象具体是什么类型一律按ob_type查表执行——同一个PyObject *参数查到的表不同行为就不同。标志位tp_flags 如何划定类型能力tp_flags是位掩码解释器用PyType_HasFeature(t, f)宏检查举三个例子Py_TPFLAGS_LIST_SUBCLASSPyList_Check(op)的实现就是PyType_FastSubclass(Py_TYPE(op), Py_TPFLAGS_LIST_SUBCLASS)不精确比类型比的是标志位Py_TPFLAGS_HAVE_GC带上它实例才会进循环垃圾回收器的追踪列表——只有对象内部持有其他PyObject *指针的类型才需要Py_TPFLAGS_IMMUTABLETYPE标记类型本身的字典不可再被运行时改写。类型的类型type 自引用int.__class__是class type而type.__class__就是type自己——类型对象的ob_type字段指回PyType_Type形成自引用闭环。元类机制只是type的子类在构造新类型时替换这个入口无需改动对象模型任何字段。设计复盘极简核心如何撑起复杂功能设计取舍在对象模型里的体现你得到什么单一职责struct _object只管ob_refcnt与ob_type逻辑全在类型表核心结构永不膨胀任何语言级新特性都不用动它组合优于继承PyObject_HEAD/PyObject_VAR_HEAD嵌套进具体结构体新类型白得计数、对齐与调度字段互不污染开放–封闭加新类型 新结构体 新PyTypeObject实例struct _object零修改stable ABI 因此成立延伸阅读Include/object.hstruct _object、PyVarObject与PyTypeObject的完整定义Include/listobject.h列表公开 API结构体本体在Include/cpython/listobject.hInclude/unicodeobject.hPEP 393 的PyASCIIObject家族与state位域说明Include/longobject.h整数公开 API布局在Include/cpython/longintrepr.h的_longobject。动手挑战假设你要实现一个可变整数MutableInt请回答三处落点——(1) 结构体里加哪个基础头才能拿到ob_size答PyVarObject即PyObject_VAR_HEAD(2)tp_flags里要置位哪一位才能被循环 GC 追踪(3) 数据缓冲的释放逻辑必须挂在哪个方法指针上。三个答案都藏在上面贴过的源码里。回到 a 42再写一次a 42你现在知道解释器做的事分一块_longobject内存把ob_type填成PyLong_Type的地址把ob_refcnt置 1。此后每一次赋值、传参、del都只是这个计数器的一收一支计数归零时tp_dealloc安静地收回内存。CPython 对象模型的全部家当就是两个字段、一张类型表和一本不断记着的账。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表