CPython C API 对象堆内存分配全解:从 PyObject_New 到 PyType_GenericAlloc 的底层实现
本篇围绕 CPython 官方 C API 文档 "Allocating objects on the heap"(位于 Doc/c-api/allocation.rst)展开,系统讲解 C 扩展在堆上分配 Python 对象的完整工具链:PyObject_Init / PyObject_InitVar / PyObject_New / PyObject_NewVar 四大接口及其在 Objects/object.c 中的源码实现。读完本文,你将掌握为新类型对象正确分配与初始化内存的全部规则,理解 tp_alloc 槽位与这些底层函数的调用关系,并能规避诸如"忘记清零"、"GC 类型误用 PyObject_New"等常见扩展开发陷阱。
堆上对象分配的基本规则
CPython 对象模型的第一条铁律写在 Include/object.h 的头部注释中:
Objects are never allocated statically or on the stack; they must be accessed through special macros and functions only.
即 Python 对象绝不能以静态存储或栈方式分配,必须通过专门的宏和函数在堆上分配(标准类型的类型对象是例外,它们以静态初始化的 PyTypeObject 存在)。该注释同时解释了对象的两条重要约束:
- 对象不会在内存中"漂移":一旦分配,其地址和大小终生不变。变长数据只能通过对象内部指针间接持有,因为对象引用就是简单指针,移动对象需要更新所有指向它的指针;
- 所有对象通过
PyObject *指针访问:PyObject结构只包含引用计数和类型指针两个字段,实际数据需把指针强转为更长的 C 结构来访问,且该长结构必须以PyObject_HEAD开头。
从 Include/object.h 可以看到 struct _object 的当前布局(以有 GIL 构建为例):
struct _object {
_Py_ANONYMOUS union {
int64_t ob_refcnt_full;
struct {
uint32_t ob_refcnt; // 引用计数(小端平台)
uint16_t ob_overflow;
uint16_t ob_flags;
};
_Py_ALIGNED_DEF(_PyObject_MIN_ALIGNMENT, char) _aligner;
};
PyTypeObject *ob_type; // 类型指针,属于 stable ABI
};
注意 _PyObject_MIN_ALIGNMENT 被设为 4 字节(Include/object.h),目的是让对象指针的最低位"腾出来"供 GC 链表指针等用途,这是分配器对字节对齐的要求。
核心接口一:PyObject_Init / PyObject_InitVar —— 只初始化,不分配
PyObject_Init(PyObject *op, PyTypeObject *type) 用于把一块你已经自行分配好的新对象内存 op 初始化上类型与初始引用计数,返回该对象。官方文档中两条关键澄清值得重点理解:
- 与
__init__无关:尽管名字相近,该函数与对象的__init__方法(tp_init槽位)完全无关,它不会调用__init__; - 属于底层例程:一般应优先使用类型的
tp_alloc槽位;实现tp_alloc时则优先使用PyType_GenericAlloc或PyObject_New。
PyObject_InitVar(PyVarObject *op, PyTypeObject *type, Py_ssize_t size) 在 PyObject_Init 的基础上额外初始化变长对象的长度信息(ob_size 字段)。两者都带有同样的警告:它们只初始化对象头部(初始 PyObject 结构体对应的内存),不会把剩余部分清零。
在 Objects/object.c 中可以看到公开函数只是对内部快速路径的薄封装:
PyObject *
PyObject_Init(PyObject *op, PyTypeObject *tp)
{
if (op == NULL) {
return PyErr_NoMemory();
}
_PyObject_Init(op, tp);
return op;
}
内部快路径 _PyObject_Init 定义在 Include/internal/pycore_object.h:
static inline void
_PyObject_Init(PyObject *op, PyTypeObject *typeobj)
{
assert(op != NULL);
Py_SET_TYPE(op, typeobj);
assert(_PyType_HasFeature(typeobj, Py_TPFLAGS_HEAPTYPE) || _Py_IsImmortal(typeobj));
_Py_INCREF_TYPE(typeobj);
_Py_NewReference(op);
}
它依次完成三件事:写入类型指针、给类型对象加引用(堆类型对象需要计数,静态类型对象是 immortal)、为新对象建立初始引用计数。_PyObject_InitVar 在此基础上再调用 Py_SET_SIZE(op, size) 写入 ob_size。这也印证了文档所述"只初始化部分内存、其余字节未定义"——如果你用自定义分配器拿到的内存没清零,对象头之后的字段就是垃圾值,后续必须由 tp_init 逻辑负责填充。
核心接口二:PyObject_New / PyObject_NewVar —— 分配 + 初始化一体
PyObject_New(TYPE, typeobj) 宏的完整行为(见 Doc/c-api/allocation.rst 及 Include/objimpl.h 的定义):
#define PyObject_New(type, typeobj) ((type *)_PyObject_New(typeobj))
- 用 C 结构类型
TYPE与 Python 类型对象typeobj(PyTypeObject*)分配新对象:内部调用PyObject_Malloc分配内存,再按PyObject_Init的方式初始化; - 调用方持有对象的唯一引用(引用计数为 1);
- 不要直接用它分配对象内存——应调用类型的
tp_alloc槽位;填充tp_alloc时优先用PyType_GenericAlloc而不是简单转发此宏的自定义函数; - 该宏不会调用
tp_alloc、tp_new(__new__)或tp_init(__init__); - 不能用于
tp_flags含Py_TPFLAGS_HAVE_GC的类型,此类对象须改用PyObject_GC_New; - 其分配的内存必须用
PyObject_Free释放(通常经由tp_free槽位); - 返回的内存不保证在初始化前已被完全清零;
- 它构造的不是完全初始化的对象,只是分配内存并等待
tp_init进一步初始化。要构造完全初始化的对象应直接调用类型本身,例如:
PyObject *foo = PyObject_CallNoArgs((PyObject *)&PyFoo_Type);
PyObject_NewVar(TYPE, typeobj, size) 与之的区别在于:它为 TYPE 结构加上 size 个 tp_itemsize 大小的字段分配足够内存,并按 PyObject_InitVar 的方式初始化(见 Include/objimpl.h)。文档特别指出它对 tuple 这类构造时即可确定大小的对象非常有用——把字段数组嵌入同一次分配,减少分配次数、提升内存管理效率。同样地,GC 类型须改用 PyObject_GC_NewVar,且不能直接用于对象分配,应走 tp_alloc。
实现层面,Objects/object.c 给出了两个函数:
PyObject *
_PyObject_New(PyTypeObject *tp)
{
PyObject *op = (PyObject *) PyObject_Malloc(_PyObject_SIZE(tp));
if (op == NULL) {
return PyErr_NoMemory();
}
_PyObject_Init(op, tp);
return op;
}
PyVarObject *
_PyObject_NewVar(PyTypeObject *tp, Py_ssize_t nitems)
{
PyVarObject *op;
const size_t size = _PyObject_VAR_SIZE(tp, nitems);
op = (PyVarObject *) PyObject_Malloc(size);
if (op == NULL) {
return (PyVarObject *)PyErr_NoMemory();
}
_PyObject_InitVar(op, tp, nitems);
return op;
}
注意两个细节:
- 分配失败会设置 MemoryError:与裸的
PyObject_Malloc不同(后者失败仅返回 NULL),_PyObject_New/_PyObject_NewVar在 OOM 时直接PyErr_NoMemory()并返回 NULL,调用方必须检查返回值; - 变长尺寸如何计算:
_PyObject_VAR_SIZE定义于 Include/cpython/objimpl.h,即tp_basicsize + nitems * tp_itemsize,并向上取整到sizeof(void *)的倍数,以保证对象尾部的指针字段平台对齐——这对str/int的子类在嵌入数据后还要追加指针的情况尤为关键。而_PyObject_SIZE就是tp_basicsize本身。
自定义分配器场景:先 Malloc 再 Init
Include/objimpl.h 的注释明确说明:当你必须使用平台 malloc 堆、共享内存或 C++ new 等特定内存管理形式时,先用自定义分配器分配,再把指针交给 PyObject_Init / PyObject_InitVar 填充 Python 相关字段。但要清楚 Python 对这些对象没有控制权——它们不参与 Python 内存管理器,可能不适用于自动 GC,必须在析构时自行保证释放。
Include/cpython/objimpl.h 给出了一个可直接参考的完整示例:
PyObject *
YourObject_New(...)
{
PyObject *op;
op = (PyObject *) Your_Allocator(_PyObject_SIZE(YourTypeStruct));
if (op == NULL) {
return PyErr_NoMemory();
}
PyObject_Init(op, &YourTypeStruct);
op->ob_field = value;
...
return op;
}
该文件头部的注释还强调了一条二进制兼容纪律:每个接口同时导出函数与宏,扩展模块应优先使用函数以保证跨版本二进制兼容;宏可能暴露内部细节换取速度,使用宏意味着每次 Python 版本升级都必须重新编译扩展。
Py_None:_Py_NoneStruct 单例
文档末尾声明了 _Py_NoneStruct(PyObject 变量):Python 层面可见的 None 对象,只能经由 Py_None 宏访问,该宏求值为指向此对象的指针(见 Doc/c-api/allocation.rst)。
在 Include/object.h 中,声明带有 /* Don't use this directly */ 注释:
PyAPI_DATA(PyObject) _Py_NoneStruct; /* Don't use this directly */
#if defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030D0000
# define Py_None Py_GetConstantBorrowed(Py_CONSTANT_NONE)
#else
# define Py_None (&_Py_NoneStruct)
#endif
可以看到在 Limited API 3.13+ 下,Py_None 已改为经 Py_GetConstantBorrowed(Py_CONSTANT_NONE) 获取借用引用(常量机制见 Include/object.h 中的 Py_CONSTANT_NONE 枚举),其余场景仍是 &_Py_NoneStruct。文档还给出配套判断宏 Py_IsNone(x),等价于 Python 的 x is None。返回 None 的标准写法是 Py_RETURN_NONE 宏——在 Limited API 3.12 及以上可直接 return Py_None(immortal 对象无需加引用),旧版本则需 return Py_NewRef(Py_None)。
推荐路径:tp_alloc 与 PyType_GenericAlloc
PyObject_New 系列文档中反复出现的建议是"避免直接调用,走 tp_alloc"。CPython 默认的通用分配器 PyType_GenericAlloc 定义在 Objects/typeobject.c:
PyObject *
PyType_GenericAlloc(PyTypeObject *type, Py_ssize_t nitems)
{
PyObject *obj = _PyType_AllocNoTrack(type, nitems);
if (obj == NULL) {
return NULL;
}
if (_PyType_IS_GC(type)) {
_PyObject_GC_TRACK(obj);
}
return obj;
}
其内部辅助函数 _PyType_AllocNoTrack(Objects/typeobject.c)展示了比 PyObject_New 更完整的现代分配流程:
- 尺寸计算包含
Py_TPFLAGS_MANAGED_WEAKREF/Py_TPFLAGS_MANAGED_DICT带来的 preheader(两个由 VM 管理的指针槽),以及Py_TPFLAGS_INLINE_VALUES的内联值区; - 用
_PyObject_MallocWithType走类型关联的内存池(内存按类型分桶,便于回收); - 对 GC 类型先
_PyObject_GC_Link建立 GC 链表节点; - 显式
memset把对象头之外的全部内存清零(Objects/typeobject.c)——这正弥补了PyObject_New系列"不保证清零"的缺口; - 按
tp_itemsize是否为 0 分别走_PyObject_Init或_PyObject_InitVar; - 返回前在
PyType_GenericAlloc中对 GC 类型执行_PyObject_GC_TRACK纳入垃圾回收追踪。
因此对扩展开发者而言,为自定义类型填充 tp_alloc 时首选 PyType_GenericAlloc;只有当你需要自定义分配语义(如自定义内存池)时,才退回到 PyObject_New/PyObject_Malloc + PyObject_Init 的组合。
软弃用的旧式别名(3.15 起 soft-deprecated)
Doc/c-api/allocation.rst 列出了自 3.15 起被软弃用的大写别名,它们纯粹是为向后兼容保留的别名,新代码应使用对应的小写形式:
| 软弃用别名 | 对应函数/宏 |
|---|---|
PyObject_NEW(type, typeobj) |
PyObject_New |
PyObject_NEW_VAR(type, typeobj, n) |
PyObject_NewVar |
PyObject_INIT(op, typeobj) |
PyObject_Init |
PyObject_INIT_VAR(op, typeobj, n) |
PyObject_InitVar |
PyObject_MALLOC(n) |
PyObject_Malloc |
PyObject_REALLOC(p, n) |
PyObject_Realloc |
PyObject_FREE(p) |
PyObject_Free |
PyObject_DEL(p) |
PyObject_Free |
PyObject_Del(p) |
PyObject_Free |
这些别名在源码中同样有对应物,例如 PyObject_MALLOC/PyObject_REALLOC/PyObject_FREE/PyObject_Del/PyObject_DEL 的定义见 Include/objimpl.h,PyObject_INIT/PyObject_NEW 见 Include/objimpl.h。注释还说明 PyObject_Del 和 PyObject_DEL 被定义为无参数形式,以便能直接用作函数指针(如 tp_free = PyObject_Del)。
相关文档
- 扩展模块的分配与创建,参见 Doc/c-api/module.rst(原文档
moduleobjects交叉引用的目标章节)。
小结:选型速查
| 场景 | 推荐接口 |
|---|---|
为类型实现 tp_alloc 槽位 |
PyType_GenericAlloc(首选) |
| 分配并初始化非 GC 定长对象 | PyObject_New |
| 分配并初始化非 GC 变长对象 | PyObject_NewVar |
| 自定义分配器 + 只初始化头部 | PyObject_Init / PyObject_InitVar |
释放对象内存(经 tp_free) |
PyObject_Free |
访问 None 单例 |
Py_None 宏,勿直接用 _Py_NoneStruct |
三条最容易踩的坑:PyObject_New 系列不保证清零、不触发 __init__、不能用于 GC 类型;PyObject_Init 只负责对象头,剩余字段必须由你的初始化代码负责;分配失败时 _PyObject_New/PyObject_Init 系列会设置 MemoryError 并返回 NULL,调用后务必判空。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00