CPython 扩展类型开发详解:从零到循环垃圾回收的 PyTypeObject 完全指南
本文基于 CPython 官方教程 newtypes_tutorial.rst 整理展开,系统讲解如何在 C 扩展模块中定义可被 Python 代码操作的新类型:从最小可运行的 Custom 类型,到添加数据成员与方法、实现细粒度属性控制、支持循环垃圾回收(cyclic GC),直至派生内置 list 类型的 SubList。读完之后,你将掌握 PyTypeObject 各 tp_ 槽位的职责、引用计数的安全写法,并能独立完成一个可安装、可测试的扩展类型模块。
一、基础原理:PyObject、类型对象与类型方法
CPython 运行时把所有 Python 对象都看作类型为 PyObject* 的变量,PyObject* 是所有 Python 对象的“基类型”。PyObject 结构体本身只包含对象的引用计数(reference count)和一个指向“类型对象”(type object)的指针——一切行为都由类型对象决定:解释器在查属性、调用方法、执行乘法等操作时,具体调用哪个 C 函数,全部由类型对象中的函数指针(称为 “type methods”)决定。因此,定义一个新的扩展类型,本质就是创建一个新的 PyTypeObject。
在源码中可以看到基础定义:object.h 第 65 行定义了每个对象结构体必须开头的头部宏:
/* PyObject_HEAD defines the initial segment of every PyObject. */
#define PyObject_HEAD PyObject ob_base;
PyObject_HEAD 定义了一个名为 ob_base、类型为 PyObject 的字段,其中保存类型指针和引用计数(分别可用宏 Py_TYPE 与 Py_REFCNT 访问)。使用宏而非直接访问字段,是为了抽象掉底层布局差异,并让 debug 构建(如开启引用追踪的构建)可以插入额外字段。
一个最小但完整、在模块 custom 中定义类型 Custom 的模块如下(完整源码见 custom.c):
#define PY_SSIZE_T_CLEAN
#include <Python.h>
typedef struct {
PyObject_HEAD
/* Type-specific fields go here. */
} CustomObject;
static PyTypeObject CustomType = {
.ob_base = PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "custom.Custom",
.tp_doc = PyDoc_STR("Custom objects"),
.tp_basicsize = sizeof(CustomObject),
.tp_itemsize = 0,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_new = PyType_GenericNew,
};
static int
custom_module_exec(PyObject *m)
{
if (PyType_Ready(&CustomType) < 0) {
return -1;
}
if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
return -1;
}
return 0;
}
static PyModuleDef_Slot custom_module_slots[] = {
{Py_mod_exec, custom_module_exec},
// Just use this while using static types
{Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
{0, NULL}
};
static PyModuleDef custom_module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "custom",
.m_doc = "Example module that creates an extension type.",
.m_size = 0,
.m_slots = custom_module_slots,
};
PyMODINIT_FUNC
PyInit_custom(void)
{
return PyModuleDef_Init(&custom_module);
}
这个文件定义了三样东西:
Custom对象的内容:CustomObject结构体,每个Custom实例分配一份;Custom类型的行为:CustomType结构体,定义一组标志和函数指针,解释器在请求特定操作时检查它们;custom模块的定义与执行:PyInit_custom函数与custom_module(PyModuleDef)结构,以及负责初始化模块对象的custom_module_exec函数。
1.1 对象结构体:PyObject_HEAD 是必须的开头
typedef struct {
PyObject_HEAD
} CustomObject;
注意宏 PyObject_HEAD 后面没有分号(宏展开后自带分号),多加一个会引来编译器抱怨。对象通常还要在标准头部之外保存自己的数据,例如标准浮点数的定义就是 PyObject_HEAD 加一个 double ob_fval 成员。
1.2 类型对象:逐字段拆解
static PyTypeObject CustomType = {
.ob_base = PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "custom.Custom",
.tp_doc = PyDoc_STR("Custom objects"),
.tp_basicsize = sizeof(CustomObject),
.tp_itemsize = 0,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_new = PyType_GenericNew,
};
官方推荐用 C99 风格指定初始化器(designated initializers),这样既不必列出所有不关心的 PyTypeObject 字段,也不用关心字段声明顺序。object.h 中 PyTypeObject 的完整定义字段远比上面多,未指定的字段会被 C 编译器补零,通常的惯例是不需要就不显式写。
逐字段说明:
-
.ob_base = PyVarObject_HEAD_INIT(NULL, 0):初始化ob_base字段的必备样板代码。 -
.tp_name = "custom.Custom":类型的名字,会出现在对象的默认文本表示和部分错误信息中,例如:>>> "" + custom.Custom() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: can only concatenate str (not "custom.Custom") to str注意这是一个点分名,同时包含模块名和模块内的类型名:这里是模块
custom中的类型Custom,所以类型名设为custom.Custom。使用真实的点分导入路径,对让类型与pydoc和pickle模块兼容非常重要。 -
.tp_basicsize = sizeof(CustomObject),.tp_itemsize = 0:让 Python 知道创建新Custom实例时要分配多少内存。tp_itemsize仅用于可变大小的对象,其余情况应为 0。官方文档在此处特别提示了一个多继承陷阱:如果你的类型希望可从 Python 子类化,且
tp_basicsize与基类相同,可能遇到多重继承问题——Python 子类必须把你的类型列在__bases__第一位,否则无法调用你的__new__。避免办法是让tp_basicsize大于基类(大多数情况天然成立,因为基类通常是object或你在基类上追加了数据成员)。 -
.tp_flags = Py_TPFLAGS_DEFAULT:所有类型都应包含这个常量,它启用了至少到 Python 3.3 为止定义的全部成员;需要更多成员时,OR 上对应标志即可。 -
.tp_doc = PyDoc_STR("Custom objects"):类型的文档字符串。 -
.tp_new = PyType_GenericNew:要允许创建对象,必须提供tp_new处理函数,它等价于 Python 方法__new__,但必须显式指定。这里直接使用 API 提供的默认实现PyType_GenericNew。
1.3 模块初始化:PyType_Ready 与 PyModule_AddObjectRef
custom_module_exec 中还有两段关键代码:
if (PyType_Ready(&CustomType) < 0) {
return -1;
}
PyType_Ready 初始化 Custom 类型,把大量成员填充为默认值,包括最初设为 NULL 的 ob_type 字段。
if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
return -1;
}
把类型加入模块字典,这样就能通过调用类来创建实例:
>>> import custom
>>> mycustom = custom.Custom()
1.4 构建与安装
把上面的代码存为 custom.c,配套文件放在 pyproject.toml:
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "custom"
version = "1"
以及 setup.py(初始版本只有一个扩展):
from setuptools import Extension, setup
setup(ext_modules=[Extension("custom", ["custom.c"])])
在 shell 中执行:
$ python -m pip install .
即可生成并安装扩展文件(如 custom.so);启动 Python 后就能 import custom 并随意摆弄 Custom 对象了。
当然,此时的 Custom 类型相当无趣:没有数据、不执行任何操作,甚至不能被子类化。
二、为类型添加数据成员与方法
这一节把基础例子扩展为带数据和方法的版本,并让类型可作为基类使用。新模块 custom2 的完整源码见 custom2.c。
2.1 对象结构:first、last 与 number
Custom 类型现在有三个数据属性:first、last 和 number。前两者是保存名与姓的 Python 字符串,number 是 C 整型:
typedef struct {
PyObject_HEAD
PyObject *first; /* first name */
PyObject *last; /* last name */
int number;
} CustomObject;
2.2 析构函数:tp_dealloc
有了要管理的数据,就必须更谨慎地处理对象分配与释放。至少要提供一个析构方法:
static void
Custom_dealloc(PyObject *op)
{
CustomObject *self = (CustomObject *) op;
Py_XDECREF(self->first);
Py_XDECREF(self->last);
Py_TYPE(self)->tp_free(self);
}
并赋给 tp_dealloc 槽:.tp_dealloc = Custom_dealloc,
该函数先释放两个 Python 属性上的引用。Py_XDECREF 能正确处理参数为 NULL 的情况(若 tp_new 中途失败就可能发生)。然后调用对象类型的 tp_free 成员(通过 Py_TYPE(self) 求得)释放对象内存。注意对象的类型未必是 CustomType——对象可能是子类实例。
教程特别强调:显式转型到 CustomObject * 是必需的,因为 tp_dealloc 函数指针期望接收 PyObject * 参数。把函数挂到类型的 tp_dealloc 槽即声明了它只会用我们自己的 CustomObject 实例调用,所以转型是安全的——这可以称为“C 语言里的面向对象多态”。旧代码中常见直接让 Custom_dealloc 接收 CustomObject *self 并在赋值槽时强转 (destructor) 的写法,在 CPython 支持的所有架构上行为相同,但按 C 标准属于未定义行为,应避免。
2.3 创建函数:tp_new 与 tp_alloc
为了让 first 和 last 初始化为空字符串,提供 tp_new 实现:
static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
CustomObject *self;
self = (CustomObject *) type->tp_alloc(type, 0);
if (self != NULL) {
self->first = PyUnicode_FromString("");
if (self->first == NULL) {
Py_DECREF(self);
return NULL;
}
self->last = PyUnicode_FromString("");
if (self->last == NULL) {
Py_DECREF(self);
return NULL;
}
self->number = 0;
}
return (PyObject *) self;
}
并安装到 .tp_new = Custom_new,。tp_new 负责创建(而非初始化)对象,在 Python 中暴露为 __new__。很多扩展类型直接复用 PyType_GenericNew;这里用 tp_new 把 first/last 初始化为非 NULL 默认值。
tp_new 接收被实例化的类型(若是子类化,不一定是 CustomType)和调用类型时传入的参数,返回创建的实例。tp_new 处理函数始终接受位置参数和关键字参数,但常常忽略它们,把参数处理留给初始化器(C 中是 tp_init,Python 中是 __init__)。tp_new 不应显式调用 tp_init,解释器会自己调用。
tp_new 实现中调用 tp_alloc 槽分配内存:
self = (CustomObject *) type->tp_alloc(type, 0);
内存分配可能失败,所以必须对 tp_alloc 结果判空。教程补充两点:
- 我们没有自己填
tp_alloc槽——PyType_Ready会从基类(默认是object)继承它。大多数类型都用默认分配策略。 - 如果你写的是协作式
tp_new(会调用基类型的tp_new或__new__),不能在运行时用 MRO 决定调用哪个方法,必须静态确定要调用的类型,直接调用其tp_new或经由type->tp_base->tp_new。否则,同时继承自其他 Python 类子的 Python 子类可能无法正确创建实例(会抛出TypeError)。
一个从源码结构可见的现代细节:教程正文示例使用 PyUnicode_FromString("") 创建空字符串,而当前仓库的 custom2.c 第 27 行已改用常量池接口 Py_GetConstant(Py_CONSTANT_EMPTY_STR) 获取共享的空字符串常量,语义相同但避免重复构造,这也说明阅读文档时可与仓库中的实际源码对照确认最新写法。
2.4 初始化函数:tp_init 与引用计数的安全替换
再定义一个接受参数的初始化函数,为实例提供初始值:
static int
Custom_init(PyObject *op, PyObject *args, PyObject *kwds)
{
CustomObject *self = (CustomObject *) op;
static char *kwlist[] = {"first", "last", "number", NULL};
PyObject *first = NULL, *last = NULL, *tmp;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOi", kwlist,
&first, &last,
&self->number))
return -1;
if (first) {
tmp = self->first;
Py_INCREF(first);
self->first = first;
Py_XDECREF(tmp);
}
if (last) {
tmp = self->last;
Py_INCREF(last);
self->last = last;
Py_XDECREF(tmp);
}
return 0;
}
并填充 .tp_init = Custom_init,。tp_init 槽在 Python 中暴露为 __init__,用于在对象创建之后做初始化。初始化器始终接受位置参数和关键字参数,成功返回 0、出错返回 -1。
与 tp_new 不同,tp_init 没有任何保证一定会被调用(例如 pickle 模块默认在反序列化时不调用 __init__),也可能被调用多次——任何人都能调用我们对象的 __init__。因此给属性赋值必须格外小心。比如像这样写是有风险的:
if (first) {
Py_XDECREF(self->first);
Py_INCREF(first);
self->first = first;
}
我们的类型并没有限制 first 成员的类型,它可以是任意对象。它的析构器可能触发会再次访问 first 的代码执行;也可能在析构期间脱离线程状态(detach thread state),让其他线程的任意代码访问并修改我们的对象。为防这种情况,我们几乎总是先把新值赋进去,再递减旧值引用计数。仅有的例外是以下三种情况:
- 完全确定引用计数大于 1;
- 确定对象析构既不会脱离线程状态,也不会回调我们类型的代码(当对象确定是字符串或浮点数等基本类型时,可以这样认为);
- 在
tp_dealloc中递减一个不支持循环垃圾回收的类型的成员引用。
顺带说明,仓库中的 custom2.c 第 55 行把上述“先存后换”模式收敛成了 Py_XSETREF(self->first, Py_NewRef(first));——这是 CPython 近年引入的新式引用管理宏,一条语句完成“建新引用 + 原子替换 + 释放旧引用”,语义与教程中的手写三步等价且更不易出错。
2.5 数据属性:PyMemberDef 与 tp_members
要把实例变量暴露为属性,最简单的方式是定义成员表:
static PyMemberDef Custom_members[] = {
{"first", Py_T_OBJECT_EX, offsetof(CustomObject, first), 0,
"first name"},
{"last", Py_T_OBJECT_EX, offsetof(CustomObject, last), 0,
"last name"},
{"number", Py_T_INT, offsetof(CustomObject, number), 0,
"custom number"},
{NULL} /* Sentinel */
};
并放入 tp_members 槽:.tp_members = Custom_members,
每个成员定义包含成员名、类型、偏移量、访问标志和文档字符串(定义见 structmember.h)。这种方式的缺点是无法限制可赋值到这些 Python 属性上的对象类型:我们期望 first/last 是字符串,但任意 Python 对象都能被赋值;而且属性可以被 del 删除,C 指针随之变为 NULL。
2.6 方法:PyMethodDef 与 tp_methods
定义单个方法 Custom.name,输出名与姓的拼接:
static PyObject *
Custom_name(PyObject *op, PyObject *Py_UNUSED(dummy))
{
CustomObject *self = (CustomObject *) op;
if (self->first == NULL) {
PyErr_SetString(PyExc_AttributeError, "first");
return NULL;
}
if (self->last == NULL) {
PyErr_SetString(PyExc_AttributeError, "last");
return NULL;
}
return PyUnicode_FromFormat("%S %S", self->first, self->last);
}
方法实现为一个 C 函数,第一个参数是 Custom(或其子类)实例——方法永远以实例作为第一参数。此处没有额外参数,因此不需要接受参数元组或关键字字典。它等价于 Python 方法:
def name(self):
return "%s %s" % (self.first, self.last)
注意必须检查 first/last 是否为 NULL,因为它们可能被删除(删除时 C 指针被置为 NULL)。更好的做法是禁止删除并把属性值限制为字符串——下一节解决。
把方法放进方法定义数组:
static PyMethodDef Custom_methods[] = {
{"name", Custom_name, METH_NOARGS,
"Return the name, combining the first and last name"
},
{NULL} /* Sentinel */
};
(使用 METH_NOARGS 标志表示方法除 self 外不接收参数)
然后赋给 tp_methods 槽:.tp_methods = Custom_methods,
2.7 允许子类化
最后,让类型可用作基类。此前写的方法都不对被操作对象的类型做假设,所以只需给类标志加上 Py_TPFLAGS_BASETYPE:
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
相应地把 PyInit_custom 改名为 PyInit_custom2,更新 PyModuleDef 中的模块名和 PyTypeObject 中的完整类名("custom2.Custom")。更新 setup.py 加入新模块后重新安装:
from setuptools import Extension, setup
setup(ext_modules=[
Extension("custom", ["custom.c"]),
Extension("custom2", ["custom2.c"]),
])
$ python -m pip install .
仓库中的 setup.py 是最终完整版,同时注册了全部五个示例扩展:
from setuptools import Extension, setup
setup(ext_modules=[
Extension("custom", ["custom.c"]),
Extension("custom2", ["custom2.c"]),
Extension("custom3", ["custom3.c"]),
Extension("custom4", ["custom4.c"]),
Extension("sublist", ["sublist.c"]),
])
三、为数据属性提供更精细的控制
上一节的版本中,first 和 last 可以被设为非字符串值,甚至被删除。这一节(完整源码见 custom3.c)用自定义 getter/setter 确保这两个属性始终包含字符串。
3.1 getter 与 setter
first 属性的存取函数:
static PyObject *
Custom_getfirst(PyObject *op, void *closure)
{
CustomObject *self = (CustomObject *) op;
Py_INCREF(self->first);
return self->first;
}
static int
Custom_setfirst(PyObject *op, PyObject *value, void *closure)
{
CustomObject *self = (CustomObject *) op;
PyObject *tmp;
if (value == NULL) {
PyErr_SetString(PyExc_TypeError, "Cannot delete the first attribute");
return -1;
}
if (!PyUnicode_Check(value)) {
PyErr_SetString(PyExc_TypeError,
"The first attribute value must be a string");
return -1;
}
tmp = self->first;
Py_INCREF(value);
self->first = value;
Py_DECREF(tmp);
return 0;
}
getter 收到 Custom 对象和一个 “closure”(void 指针)。这里闭包被忽略;闭包支持的进阶用法是把定义期数据传给 getter/setter,例如让一组 getter/setter 函数根据闭包中的数据决定操作哪个属性。setter 收到 Custom 对象、新值和闭包;新值可能为 NULL,此时表示属性正在被删除。我们的 setter 在删除属性或新值不是字符串时抛出错误。
3.2 PyGetSetDef 与 tp_getset
创建 PyGetSetDef 结构体数组:
static PyGetSetDef Custom_getsetters[] = {
{"first", Custom_getfirst, Custom_setfirst,
"first name", NULL},
{"last", Custom_getlast, Custom_setlast,
"last name", NULL},
{NULL} /* Sentinel */
};
并注册到 tp_getset 槽:.tp_getset = Custom_getsetters,
PyGetSetDef 结构的最后一项就是上面说的闭包;此处不使用,传 NULL。
同时移除这两个属性的成员定义:
static PyMemberDef Custom_members[] = {
{"number", Py_T_INT, offsetof(CustomObject, number), 0,
"custom number"},
{NULL} /* Sentinel */
};
3.3 同步收紧 tp_init
还要更新 tp_init 处理函数,用格式码 U(仅接受 str)替代 O,只允许传字符串:
static int
Custom_init(PyObject *op, PyObject *args, PyObject *kwds)
{
CustomObject *self = (CustomObject *) op;
static char *kwlist[] = {"first", "last", "number", NULL};
PyObject *first = NULL, *last = NULL, *tmp;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|UUi", kwlist,
&first, &last,
&self->number))
return -1;
if (first) {
tmp = self->first;
Py_INCREF(first);
self->first = first;
Py_DECREF(tmp);
}
if (last) {
tmp = self->last;
Py_INCREF(last);
self->last = last;
Py_DECREF(tmp);
}
return 0;
}
经过这些修改,可以确信 first 和 last 成员永远不会是 NULL,于是绝大多数 Py_XDECREF 调用都可以换成 Py_DECREF(仓库的 custom3.c 第 55 行实际使用了等价的 Py_SETREF 宏完成替换)。唯一不能改的地方是 tp_dealloc:那里仍存在 tp_new 中成员初始化失败导致指针为 NULL 的可能。
教程脚注还解释了一个容易忽略的点:即便把属性限制为字符串,用户仍可传入 str 的子类实例。虽然析构普通字符串不会回调我们的对象,但无法保证析构字符串子类实例时不会回调进来——所以“先赋值后释放”的谨慎写法依然保留。
再按惯例重命名模块初始化函数与模块名,并在 setup.py 中追加 custom3 的定义即可。
四、支持循环垃圾回收
Python 有循环垃圾回收器(cyclic GC),即使引用计数不为零也能识别无用对象,这发生在对象参与引用环时。例如:
>>> l = []
>>> l.append(l)
>>> del l
这个列表包含自身。删除后它仍持有自引用,引用计数不会归零;幸运的是,循环垃圾回收器最终会判定它是垃圾并释放。
在第二版 Custom 示例中,first/last 可存放任意类型对象;在第二、三版中又允许子类化,而子类可以添加任意属性。这两个原因都足以让 Custom 对象参与引用环:
>>> import custom3
>>> class Derived(custom3.Custom): pass
...
>>> n = Derived()
>>> n.some_attribute = n
要让参与引用环的 Custom 实例被正确检测并回收,Custom 类型需要再填两个槽并启用对应标志(完整源码见 custom4.c)。
4.1 遍历函数 tp_traverse
遍历函数让循环 GC 知道哪些子对象可能参与环:
static int
Custom_traverse(PyObject *op, visitproc visit, void *arg)
{
CustomObject *self = (CustomObject *) op;
int vret;
if (self->first) {
vret = visit(self->first, arg);
if (vret != 0)
return vret;
}
if (self->last) {
vret = visit(self->last, arg);
if (vret != 0)
return vret;
}
return 0;
}
对每个可能参与环的子对象,都要调用传入的 visit 函数:它接收子对象和传给遍历函数的额外参数 arg,返回非零值时必须原样返回。
Python 提供 Py_VISIT 宏来自动完成 visit 调用,大幅减少样板代码:
static int
Custom_traverse(PyObject *op, visitproc visit, void *arg)
{
CustomObject *self = (CustomObject *) op;
Py_VISIT(self->first);
Py_VISIT(self->last);
return 0;
}
注意:tp_traverse 实现要想使用 Py_VISIT,其参数必须恰好命名为 visit 和 arg。
4.2 清空函数 tp_clear 与 Py_CLEAR
需要提供清空可参与环的子对象的方法:
static int
Custom_clear(PyObject *op)
{
CustomObject *self = (CustomObject *) op;
Py_CLEAR(self->first);
Py_CLEAR(self->last);
return 0;
}
注意这里使用了 Py_CLEAR 宏——它是清空任意类型数据属性并递减引用计数的推荐且安全的方式。如果在置空之前先调用 Py_XDECREF,属性的析构器可能回调再次读取该属性的代码(引用环存在时尤其危险)。可以这样模拟 Py_CLEAR:
PyObject *tmp;
tmp = self->first;
self->first = NULL;
Py_XDECREF(tmp);
但总是用 Py_CLEAR 删除属性要简单得多、出错概率低得多,不要为了微优化牺牲健壮性。
4.3 重写析构函数并启用 Py_TPFLAGS_HAVE_GC
析构器在清空属性时可能执行任意代码,这意味着 GC 可能在该函数内部被触发。由于 GC 假定对象引用计数非零,必须在清空成员之前调用 PyObject_GC_UnTrack 把对象从 GC 中注销。重写后的析构函数:
static void
Custom_dealloc(PyObject *op)
{
PyObject_GC_UnTrack(op);
(void)Custom_clear(op);
Py_TYPE(op)->tp_free(op);
}
最后给类标志加上 Py_TPFLAGS_HAVE_GC:
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
基本就齐了。如果写过自定义 tp_alloc 或 tp_free 处理函数,还要为循环 GC 修改它们;大多数扩展直接使用自动提供的版本即可。
仓库中的 test.py 用 doctest 验证了 GC 行为:构造 custom4.Custom 子类并人为制造 s.cycle = [s] 的引用环后禁用 GC,删除 s 后目标对象引用计数仍高于基线;gc.collect() 之后引用计数回落,证明该扩展类型确实能被循环 GC 正确回收。
五、派生其他类型:以 list 为基类的 SubList
可以创建从现有类型派生的新扩展类型。继承内置类型最容易,因为扩展可以直接使用所需的 PyTypeObject;在多个扩展模块之间共享这些结构则相对困难。
本例创建从内置 list 类型继承的 SubList 类型(完整源码见 sublist.c)。新类型与常规列表完全兼容,但额外提供 increment 方法,递增一个内部计数器:
>>> import sublist
>>> s = sublist.SubList(range(3))
>>> s.extend(s)
>>> print(len(s))
6
>>> print(s.increment())
1
>>> print(s.increment())
2
#define PY_SSIZE_T_CLEAN
#include <Python.h>
typedef struct {
PyListObject list;
int state;
} SubListObject;
static PyObject *
SubList_increment(PyObject *op, PyObject *Py_UNUSED(dummy))
{
SubListObject *self = (SubListObject *) op;
self->state++;
return PyLong_FromLong(self->state);
}
static PyMethodDef SubList_methods[] = {
{"increment", SubList_increment, METH_NOARGS,
PyDoc_STR("increment state counter")},
{NULL},
};
static int
SubList_init(PyObject *op, PyObject *args, PyObject *kwds)
{
SubListObject *self = (SubListObject *) op;
if (PyList_Type.tp_init(op, args, kwds) < 0)
return -1;
self->state = 0;
return 0;
}
static PyTypeObject SubListType = {
.ob_base = PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "sublist.SubList",
.tp_doc = PyDoc_STR("SubList objects"),
.tp_basicsize = sizeof(SubListObject),
.tp_itemsize = 0,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_init = SubList_init,
.tp_methods = SubList_methods,
};
源码与前面 Custom 例子高度相似,主要差异如下。
5.1 基类结构体必须位于首位
typedef struct {
PyListObject list;
int state;
} SubListObject;
派生类型对象的对象结构体中,基类结构体必须放在第一个成员位置。基类结构体自身已经以 PyObject_HEAD 开头。
这样,当一个 Python 对象是 SubList 实例时,其 PyObject * 指针可以安全地转型为 PyListObject * 和 SubListObject * 二者:
static int
SubList_init(PyObject *op, PyObject *args, PyObject *kwds)
{
SubListObject *self = (SubListObject *) op;
if (PyList_Type.tp_init(op, args, kwds) < 0)
return -1;
self->state = 0;
return 0;
}
上面演示了如何调用到基类类型的 __init__ 方法。
5.2 tp_base 必须在模块执行函数中赋值
写带自定义 tp_new 和 tp_dealloc 的类型时,这个模式很重要:tp_new 处理函数不应用自己的 tp_alloc 实际创建对象内存,而应通过调用基类自己的 tp_new 让基类处理分配。
PyTypeObject 结构体支持 tp_base 槽,指定类型的直接基类。由于跨平台编译问题,不能直接把这个字段初始化为指向 PyList_Type 的引用,应在 Py_mod_exec 函数中设置:
static int
sublist_module_exec(PyObject *m)
{
SubListType.tp_base = &PyList_Type;
if (PyType_Ready(&SubListType) < 0) {
return -1;
}
if (PyModule_AddObjectRef(m, "SubList", (PyObject *) &SubListType) < 0) {
return -1;
}
return 0;
}
调用 PyType_Ready 之前,必须填好 tp_base 槽。派生已有类型时,不必用 PyType_GenericNew 填充 tp_alloc 槽——基类的分配函数会被继承。
之后,调用 PyType_Ready 并把类型对象加入模块,与 Custom 基础例子完全相同。
六、小结:一个扩展类型模块的完整要素
把五节内容串联起来,一个可发布的 CPython 静态扩展类型模块需要齐备以下要素,各要素在教程配套文件中的位置可以逐一核对:
- 对象结构体:以
PyObject_HEAD开头(派生类型则以基类结构体开头),见 custom.c、sublist.c; - 类型对象:
PyTypeObject用 C99 指定初始化器填充,按需设置tp_name(使用真实点分路径,兼顾pydoc/pickle)、tp_basicsize、tp_flags、tp_new/tp_init/tp_dealloc、tp_members/tp_methods/tp_getset、tp_traverse/tp_clear; - 模块定义:
PyModuleDef+Py_mod_exec槽 +PyInit_*入口,在其中先PyType_Ready再PyModule_AddObjectRef(派生类型先填tp_base); - 打包与安装:
pyproject.toml(setuptools 构建后端)+setup.py的Extension声明,python -m pip install .安装; - 行为验证:仓库配套的 test.py 覆盖属性读写/删除、类型约束报错(
TypeError: The first attribute value must be a string、TypeError: an integer is required (got type str))和循环 GC 回收等全部边界行为。
参考路径一览:教程正文 newtypes_tutorial.rst;示例源码 custom.c、custom2.c、custom3.c、custom4.c、sublist.c;构建文件 pyproject.toml 与 setup.py;基础头文件 object.h(PyObject_HEAD,第 65 行)与 structmember.h。
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 StartedRust0624
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