CPython 扩展类型开发详解:从零到循环垃圾回收的 PyTypeObject 完全指南
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
本文基于 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。
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考