首页
/ CPython C API 对象堆内存分配全解:从 PyObject_New 到 PyType_GenericAlloc 的底层实现

CPython C API 对象堆内存分配全解:从 PyObject_New 到 PyType_GenericAlloc 的底层实现

2026-09-03 16:04:52作者:秋泉律Samson

本篇围绕 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 初始化上类型与初始引用计数,返回该对象。官方文档中两条关键澄清值得重点理解:

  1. __init__ 无关:尽管名字相近,该函数与对象的 __init__ 方法(tp_init 槽位)完全无关,它不会调用 __init__
  2. 属于底层例程:一般应优先使用类型的 tp_alloc 槽位;实现 tp_alloc 时则优先使用 PyType_GenericAllocPyObject_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.rstInclude/objimpl.h 的定义):

#define PyObject_New(type, typeobj) ((type *)_PyObject_New(typeobj))
  • 用 C 结构类型 TYPE 与 Python 类型对象 typeobjPyTypeObject*)分配新对象:内部调用 PyObject_Malloc 分配内存,再按 PyObject_Init 的方式初始化;
  • 调用方持有对象的唯一引用(引用计数为 1);
  • 不要直接用它分配对象内存——应调用类型的 tp_alloc 槽位;填充 tp_alloc 时优先用 PyType_GenericAlloc 而不是简单转发此宏的自定义函数;
  • 该宏不会调用 tp_alloctp_new__new__)或 tp_init__init__);
  • 不能用于 tp_flagsPy_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 结构加上 sizetp_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_NoneStructPyObject 变量):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_AllocNoTrackObjects/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.hPyObject_INIT/PyObject_NEWInclude/objimpl.h。注释还说明 PyObject_DelPyObject_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,调用后务必判空。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384