首页
/ CPython 扩展类型开发详解:从零到循环垃圾回收的 PyTypeObject 完全指南

CPython 扩展类型开发详解:从零到循环垃圾回收的 PyTypeObject 完全指南

2026-09-06 15:34:56作者:房伟宁

本文基于 CPython 官方教程 newtypes_tutorial.rst 整理展开,系统讲解如何在 C 扩展模块中定义可被 Python 代码操作的新类型:从最小可运行的 Custom 类型,到添加数据成员与方法、实现细粒度属性控制、支持循环垃圾回收(cyclic GC),直至派生内置 list 类型的 SubList。读完之后,你将掌握 PyTypeObjecttp_ 槽位的职责、引用计数的安全写法,并能独立完成一个可安装、可测试的扩展类型模块。

一、基础原理: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_TYPEPy_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);
}

这个文件定义了三样东西:

  1. Custom 对象的内容:CustomObject 结构体,每个 Custom 实例分配一份;
  2. Custom 类型的行为:CustomType 结构体,定义一组标志和函数指针,解释器在请求特定操作时检查它们;
  3. custom 模块的定义与执行:PyInit_custom 函数与 custom_modulePyModuleDef)结构,以及负责初始化模块对象的 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.hPyTypeObject 的完整定义字段远比上面多,未指定的字段会被 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。使用真实的点分导入路径,对让类型与 pydocpickle 模块兼容非常重要。

  • .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 类型,把大量成员填充为默认值,包括最初设为 NULLob_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 类型现在有三个数据属性:firstlastnumber。前两者是保存名与姓的 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

为了让 firstlast 初始化为空字符串,提供 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_newfirst/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"]),
])

三、为数据属性提供更精细的控制

上一节的版本中,firstlast 可以被设为非字符串值,甚至被删除。这一节(完整源码见 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;
}

经过这些修改,可以确信 firstlast 成员永远不会是 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,其参数必须恰好命名为 visitarg

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_alloctp_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_newtp_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 静态扩展类型模块需要齐备以下要素,各要素在教程配套文件中的位置可以逐一核对:

  1. 对象结构体:以 PyObject_HEAD 开头(派生类型则以基类结构体开头),见 custom.csublist.c
  2. 类型对象PyTypeObject 用 C99 指定初始化器填充,按需设置 tp_name(使用真实点分路径,兼顾 pydoc/pickle)、tp_basicsizetp_flagstp_new/tp_init/tp_dealloctp_members/tp_methods/tp_getsettp_traverse/tp_clear
  3. 模块定义PyModuleDef + Py_mod_exec 槽 + PyInit_* 入口,在其中先 PyType_ReadyPyModule_AddObjectRef(派生类型先填 tp_base);
  4. 打包与安装pyproject.toml(setuptools 构建后端)+ setup.pyExtension 声明,python -m pip install . 安装;
  5. 行为验证:仓库配套的 test.py 覆盖属性读写/删除、类型约束报错(TypeError: The first attribute value must be a stringTypeError: an integer is required (got type str))和循环 GC 回收等全部边界行为。

参考路径一览:教程正文 newtypes_tutorial.rst;示例源码 custom.ccustom2.ccustom3.ccustom4.csublist.c;构建文件 pyproject.tomlsetup.py;基础头文件 object.hPyObject_HEAD,第 65 行)与 structmember.h

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