2. 定义扩展类型:教程

Python 允许 C 扩展模块的编写者定义可以在 Python 代码中操作的新类型,类似于内置的 strlist 类型。所有扩展类型的代码都遵循一个模式,但您需要了解一些细节才能开始。本文档是对该主题的入门介绍。

2.1. 基础知识

CPython 运行时将所有 Python 对象视为类型为 PyObject* 的变量,它充当所有 Python 对象的“基类型”。PyObject 结构本身只包含对象的引用计数和指向对象“类型对象”的指针。这是关键所在;类型对象决定了当解释器查找对象的属性、调用方法或将其乘以另一个对象时,会调用哪些 (C) 函数。这些 C 函数被称为“类型方法”。

因此,如果您想定义一个新的扩展类型,您需要创建一个新的类型对象。

这种事情只能通过例子来解释,所以这里是一个最小但完整的模块,它在一个 C 扩展模块 custom 中定义了一个名为 Custom 的新类型。

备注

我们这里展示的是定义 静态 扩展类型的传统方式。它应该足以满足大多数用途。C API 还允许使用 PyType_FromSpec() 函数定义堆分配的扩展类型,本教程不涉及此函数。

#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_module 结构体,以及用于设置全新模块对象的 custom_module_exec 函数。

第一部分是

typedef struct {
    PyObject_HEAD
} CustomObject;

这是一个 Custom 对象将包含的内容。PyObject_HEAD 是每个对象结构开头强制性的,它定义了一个名为 ob_base 的字段,类型为 PyObject,包含一个指向类型对象和引用计数的指针(可以使用宏 Py_TYPEPy_REFCNT 分别访问它们)。使用宏的原因是为了抽象布局并允许在调试构建中添加额外字段。

备注

PyObject_HEAD 宏后面没有分号。请注意不要意外添加分号:有些编译器会报错。

当然,对象除了标准的 PyObject_HEAD 样板之外,通常还会存储额外的数据;例如,以下是标准 Python 浮点数的定义:

typedef struct {
    PyObject_HEAD
    double ob_fval;
} PyFloatObject;

第二部分是类型对象的定义。

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 风格的指定初始化器,以避免列出所有您不关心的 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 仅用于可变大小的对象,否则应为零。

备注

如果您的类型可以从 Python 进行子类化,并且您的类型与其基类型具有相同的 tp_basicsize,则您可能会遇到多重继承问题。您的类型的 Python 子类必须在其 __bases__ 中首先列出您的类型,否则它将无法调用您类型的 __new__() 方法而会报错。您可以通过确保您的类型具有比其基类型更大的 tp_basicsize 值来避免此问题。大多数情况下,这都是成立的,因为您的基类型要么是 object,要么您将向基类型添加数据成员,从而增加其大小。

我们将类标志设置为 Py_TPFLAGS_DEFAULT

.tp_flags = Py_TPFLAGS_DEFAULT,

所有类型都应在其标志中包含此常量。它启用了至少 Python 3.3 之前定义的所有成员。如果您需要更多成员,则需要对相应的标志进行 OR 运算。

我们在 tp_doc 中提供了类型的文档字符串。

.tp_doc = PyDoc_STR("Custom objects"),

为了启用对象创建,我们必须提供一个 tp_new 处理器。这相当于 Python 方法 __new__(),但必须明确指定。在这种情况下,我们可以直接使用 API 函数 PyType_GenericNew() 提供的默认实现。

.tp_new = PyType_GenericNew,

文件中其他所有内容都应该很熟悉,除了 custom_module_exec() 中的一些代码

if (PyType_Ready(&CustomType) < 0) {
    return -1;
}

这会初始化 Custom 类型,将多个成员填充为适当的默认值,包括我们最初设置为 NULLob_type

if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
    return -1;
}

这会将类型添加到模块字典中。这允许我们通过调用 Custom 类来创建 Custom 实例。

>>> import custom
>>> mycustom = custom.Custom()

就这样!剩下的就是构建它;将上面的代码放入一个名为 custom.c 的文件中,

[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[project]
name = "custom"
version = "1"

在一个名为 pyproject.toml 的文件中,以及

from setuptools import Extension, setup
setup(ext_modules=[Extension("custom", ["custom.c"])])

在一个名为 setup.py 的文件中;然后输入

$ python -m pip install .

在 shell 中,应该会在子目录中生成一个名为 custom.so 的文件并安装它;现在启动 Python — 您应该能够 import custom 并使用 Custom 对象。

这不是很困难,不是吗?

当然,当前的 Custom 类型相当无趣。它没有数据,也不做任何事情。甚至不能被子类化。

2.2. 在基本示例中添加数据和方法

让我们扩展基本示例,添加一些数据和方法。我们还要使该类型可以用作基类。我们将创建一个新模块 custom2,它将添加这些功能。

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stddef.h> /* for offsetof() */

typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    int number;
} CustomObject;

static void
Custom_dealloc(PyObject *op)
{
    CustomObject *self = (CustomObject *) op;
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free(self);
}

static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

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;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;

    if (first) {
        Py_XSETREF(self->first, Py_NewRef(first));
    }
    if (last) {
        Py_XSETREF(self->last, Py_NewRef(last));
    }
    return 0;
}

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 */
};

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);
}

static PyMethodDef Custom_methods[] = {
    {"name", Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* Sentinel */
};

static PyTypeObject CustomType = {
    .ob_base = PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom2.Custom",
    .tp_doc = PyDoc_STR("Custom objects"),
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_new = Custom_new,
    .tp_init = Custom_init,
    .tp_dealloc = Custom_dealloc,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
};

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},
    {Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
    {0, NULL}
};

static PyModuleDef custom_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "custom2",
    .m_doc = "Example module that creates an extension type.",
    .m_size = 0,
    .m_slots = custom_module_slots,
};

PyMODINIT_FUNC
PyInit_custom2(void)
{
    return PyModuleDef_Init(&custom_module);
}

此版本的模块有许多更改。

Custom 类型现在在其 C 结构中包含三个数据属性:firstlastnumberfirstlast 变量是包含名字和姓氏的 Python 字符串。 number 属性是一个 C 整数。

对象结构相应地更新

typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    int number;
} CustomObject;

因为我们现在有数据需要管理,所以我们必须更加谨慎地处理对象的分配和释放。至少,我们需要一个释放方法:

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 中途失败,则可能发生这种情况)。然后它调用对象类型(由 Py_TYPE(self) 计算)的 tp_free 成员来释放对象的内存。请注意,对象的类型可能不是 CustomType,因为该对象可能是子类的实例。

备注

上述到 CustomObject * 的显式类型转换是必要的,因为我们定义 Custom_dealloc 接受 PyObject * 参数,正如 tp_dealloc 函数指针期望接收 PyObject * 参数一样。通过赋值给类型的 tp_dealloc 槽,我们声明它只能使用我们的 CustomObject 类的实例进行调用,因此转换为 (CustomObject *) 是安全的。这就是 C 语言中的面向对象多态性!

在现有代码或本教程的早期版本中,您可能会看到类似的函数直接采用子类型对象结构 (CustomObject*) 的指针,如下所示:

Custom_dealloc(CustomObject *self)
{
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free((PyObject *) self);
}
...
.tp_dealloc = (destructor) Custom_dealloc,

这在 CPython 支持的所有架构上执行相同的操作,但根据 C 标准,它会调用未定义的行为。

我们希望确保名字和姓氏被初始化为空字符串,因此我们提供一个 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 成员中

.tp_new = Custom_new,

tp_new 处理程序负责创建(而不是初始化)该类型的对象。它在 Python 中作为 __new__() 方法公开。它不需要定义 tp_new 成员,实际上许多扩展类型会简单地重用 PyType_GenericNew(),就像上面 Custom 类型的第一个版本所做的那样。在这种情况下,我们使用 tp_new 处理程序将 firstlast 属性初始化为非 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 结果是否为 NULL

备注

我们没有自己填写 tp_alloc 槽。而是 PyType_Ready() 通过从我们的基类(默认情况下是 object)继承它来为我们填充它。大多数类型使用默认的分配策略。

备注

如果您正在创建一个协作式 tp_new(一个调用基类的 tp_new__new__() 的函数),则 绝不能 在运行时使用方法解析顺序来确定要调用的方法。始终静态地确定要调用的类型,并直接调用其 tp_new,或者通过 type->tp_base->tp_new 调用。如果您不这样做,则您的类型的 Python 子类(也继承自其他 Python 定义的类)可能无法正常工作。(具体来说,您可能无法创建此类子类的实例而不会遇到 TypeError。)

我们还定义了一个初始化函数,它接受参数来为我们的实例提供初始值。

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 槽。

.tp_init = Custom_init,

tp_init 槽在 Python 中作为 __init__() 方法公开。它用于在对象创建后对其进行初始化。初始化器总是接受位置参数和关键字参数,并且在成功时应返回 0,在失败时应返回 -1

tp_new 处理程序不同,不能保证 tp_init 会被调用(例如,pickle 模块默认不会在未解封的实例上调用 __init__())。它也可能被多次调用。任何人都可以对我们的对象调用 __init__() 方法。因此,在赋值新的属性值时,我们必须格外小心。例如,我们可能会试图这样赋值 first 成员:

if (first) {
    Py_XDECREF(self->first);
    Py_INCREF(first);
    self->first = first;
}

但这将是危险的。我们的类型不限制 first 成员的类型,因此它可以是任何类型的对象。它可能有一个析构函数,导致执行试图访问 first 成员的代码;或者该析构函数可能会分离线程状态,并允许任意代码在其他线程中运行,这些代码访问和修改我们的对象。

为了安全起见,并保护我们自己免受这种可能性,我们几乎总是在递减引用计数之前重新分配成员。我们什么时候不必这样做?

  • 当我们绝对知道引用计数大于1时;

  • 当我们知道对象的释放 [1] 既不会分离线程状态,也不会导致对我们类型的代码的任何回调;

  • 在不支持循环垃圾回收的类型的 tp_dealloc 处理器中递减引用计数时 [2]

我们希望将我们的实例变量公开为属性。有多种方法可以做到这一点。最简单的方法是定义成员定义:

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,

每个成员定义都包含成员名称、类型、偏移量、访问标志和文档字符串。详见下文的通用属性管理部分。

这种方法的缺点是,它无法限制可以分配给 Python 属性的对象的类型。我们期望名字和姓氏是字符串,但可以分配任何 Python 对象。此外,属性可以被删除,将 C 指针设置为 NULL。尽管我们可以确保成员被初始化为非 NULL 值,但如果属性被删除,成员可以被设置为 NULL

我们定义了一个方法 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(或 Custom 子类)实例作为第一个参数。方法总是将实例作为第一个参数。方法通常也接受位置参数和关键字参数,但在本例中我们不接受任何参数,也不需要接受位置参数元组或关键字参数字典。此方法等同于 Python 方法:

def name(self):
    return "%s %s" % (self.first, self.last)

请注意,我们必须检查 firstlast 成员是否为 NULL。这是因为它们可以被删除,在这种情况下它们被设置为 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,

最后,我们将使我们的类型可用作子类化的基类。到目前为止,我们已仔细编写了方法,使其不会对正在创建或使用的对象的类型做出任何假设,因此我们只需将 Py_TPFLAGS_BASETYPE 添加到我们的类标志定义中

.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,

我们将 PyInit_custom() 重命名为 PyInit_custom2(),更新 PyModuleDef 结构中的模块名称,并更新 PyTypeObject 结构中的完整类名称。

最后,我们更新我们的 setup.py 文件以包含新模块,

from setuptools import Extension, setup
setup(ext_modules=[
    Extension("custom", ["custom.c"]),
    Extension("custom2", ["custom2.c"]),
])

然后我们重新安装,以便我们可以 import custom2

$ python -m pip install .

2.3. 提供对数据属性的更精细控制

在本节中,我们将更精细地控制 Custom 示例中 firstlast 属性的设置方式。在我们模块的先前版本中,实例变量 firstlast 可以设置为非字符串值甚至被删除。我们希望确保这些属性始终包含字符串。

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stddef.h> /* for offsetof() */

typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    int number;
} CustomObject;

static void
Custom_dealloc(PyObject *op)
{
    CustomObject *self = (CustomObject *) op;
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free(self);
}

static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

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;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|UUi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;

    if (first) {
        Py_SETREF(self->first, Py_NewRef(first));
    }
    if (last) {
        Py_SETREF(self->last, Py_NewRef(last));
    }
    return 0;
}

static PyMemberDef Custom_members[] = {
    {"number", Py_T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};

static PyObject *
Custom_getfirst(PyObject *op, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    return Py_NewRef(self->first);
}

static int
Custom_setfirst(PyObject *op, PyObject *value, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    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;
    }
    Py_SETREF(self->first, Py_NewRef(value));
    return 0;
}

static PyObject *
Custom_getlast(PyObject *op, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    return Py_NewRef(self->last);
}

static int
Custom_setlast(PyObject *op, PyObject *value, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the last attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The last attribute value must be a string");
        return -1;
    }
    Py_SETREF(self->last, Py_NewRef(value));
    return 0;
}

static PyGetSetDef Custom_getsetters[] = {
    {"first", Custom_getfirst, Custom_setfirst,
     "first name", NULL},
    {"last", Custom_getlast, Custom_setlast,
     "last name", NULL},
    {NULL}  /* Sentinel */
};

static PyObject *
Custom_name(PyObject *op, PyObject *Py_UNUSED(dummy))
{
    CustomObject *self = (CustomObject *) op;
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}

static PyMethodDef Custom_methods[] = {
    {"name", Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* Sentinel */
};

static PyTypeObject CustomType = {
    .ob_base = PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom3.Custom",
    .tp_doc = PyDoc_STR("Custom objects"),
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_new = Custom_new,
    .tp_init = Custom_init,
    .tp_dealloc = Custom_dealloc,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
    .tp_getset = Custom_getsetters,
};

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},
    {Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
    {0, NULL}
};

static PyModuleDef custom_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "custom3",
    .m_doc = "Example module that creates an extension type.",
    .m_size = 0,
    .m_slots = custom_module_slots,
};

PyMODINIT_FUNC
PyInit_custom3(void)
{
    return PyModuleDef_Init(&custom_module);
}

为了对 firstlast 属性提供更大的控制,我们将使用自定义 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 对象和一个“闭包”,闭包是一个 void 指针。在这种情况下,闭包被忽略。(闭包支持一种高级用法,其中定义数据被传递给 getter 和 setter。例如,这可以用于允许一组 getter 和 setter 函数,它们根据闭包中的数据决定获取或设置哪个属性。)

setter 函数被传递 Custom 对象、新值和闭包。新值可能为 NULL,在这种情况下,该属性将被删除。在我们的 setter 中,如果属性被删除或其新值不是字符串,我们将引发错误。

我们创建了一个 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 */
};

我们还需要更新 tp_init 处理器,只允许传递字符串 [3]

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,因此我们几乎在所有情况下都可以删除对 NULL 值的检查。这意味着大多数 Py_XDECREF() 调用可以转换为 Py_DECREF() 调用。唯一不能更改这些调用的地方是在 tp_dealloc 实现中,因为这些成员在 tp_new 中初始化失败的可能性。

我们还像以前一样,重命名了模块初始化函数和初始化函数中的模块名称,并且在 setup.py 文件中添加了一个额外的定义。

2.4. 支持循环垃圾回收

Python 有一个循环垃圾回收器 (GC),即使对象的引用计数不为零,它也能识别不需要的对象。当对象参与循环时,可能会发生这种情况。例如,考虑:

>>> l = []
>>> l.append(l)
>>> del l

在这个例子中,我们创建了一个包含自身的列表。当我们删除它时,它仍然有来自自身的引用。它的引用计数不会降到零。幸运的是,Python 的循环垃圾回收器最终会发现该列表是垃圾并释放它。

Custom 示例的第二个版本中,我们允许任何类型的对象存储在 firstlast 属性中 [4]。此外,在第二和第三个版本中,我们允许子类化 Custom,并且子类可以添加任意属性。由于这两个原因中的任何一个,Custom 对象都可以参与循环。

>>> import custom3
>>> class Derived(custom3.Custom): pass
...
>>> n = Derived()
>>> n.some_attribute = n

为了使参与引用循环的 Custom 实例能够被循环 GC 正确检测和回收,我们的 Custom 类型需要填充两个额外的槽位并启用一个标志来启用这些槽位。

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stddef.h> /* for offsetof() */

typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    int number;
} CustomObject;

static int
Custom_traverse(PyObject *op, visitproc visit, void *arg)
{
    CustomObject *self = (CustomObject *) op;
    Py_VISIT(self->first);
    Py_VISIT(self->last);
    return 0;
}

static int
Custom_clear(PyObject *op)
{
    CustomObject *self = (CustomObject *) op;
    Py_CLEAR(self->first);
    Py_CLEAR(self->last);
    return 0;
}

static void
Custom_dealloc(PyObject *op)
{
    PyObject_GC_UnTrack(op);
    (void)Custom_clear(op);
    Py_TYPE(op)->tp_free(op);
}

static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

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;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|UUi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;

    if (first) {
        Py_SETREF(self->first, Py_NewRef(first));
    }
    if (last) {
        Py_SETREF(self->last, Py_NewRef(last));
    }
    return 0;
}

static PyMemberDef Custom_members[] = {
    {"number", Py_T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};

static PyObject *
Custom_getfirst(PyObject *op, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    return Py_NewRef(self->first);
}

static int
Custom_setfirst(PyObject *op, PyObject *value, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    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;
    }
    Py_XSETREF(self->first, Py_NewRef(value));
    return 0;
}

static PyObject *
Custom_getlast(PyObject *op, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    return Py_NewRef(self->last);
}

static int
Custom_setlast(PyObject *op, PyObject *value, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the last attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The last attribute value must be a string");
        return -1;
    }
    Py_XSETREF(self->last, Py_NewRef(value));
    return 0;
}

static PyGetSetDef Custom_getsetters[] = {
    {"first", Custom_getfirst, Custom_setfirst,
     "first name", NULL},
    {"last", Custom_getlast, Custom_setlast,
     "last name", NULL},
    {NULL}  /* Sentinel */
};

static PyObject *
Custom_name(PyObject *op, PyObject *Py_UNUSED(dummy))
{
    CustomObject *self = (CustomObject *) op;
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}

static PyMethodDef Custom_methods[] = {
    {"name", Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* Sentinel */
};

static PyTypeObject CustomType = {
    .ob_base = PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom4.Custom",
    .tp_doc = PyDoc_STR("Custom objects"),
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
    .tp_new = Custom_new,
    .tp_init = Custom_init,
    .tp_dealloc = Custom_dealloc,
    .tp_traverse = Custom_traverse,
    .tp_clear = Custom_clear,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
    .tp_getset = Custom_getsetters,
};

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},
    {Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
    {0, NULL}
};

static PyModuleDef custom_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "custom4",
    .m_doc = "Example module that creates an extension type.",
    .m_size = 0,
    .m_slots = custom_module_slots,
};

PyMODINIT_FUNC
PyInit_custom4(void)
{
    return PyModuleDef_Init(&custom_module);
}

首先,遍历方法让循环 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() 函数,该函数被传递给遍历方法。visit() 函数接受子对象和传递给遍历方法的额外参数 arg 作为参数。它返回一个整数值,如果该值非零,则必须返回。

Python 提供了一个 Py_VISIT() 宏,它自动化调用访问函数。使用 Py_VISIT(),我们可以最小化 Custom_traverse 中的样板代码

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 的实现必须将其参数准确命名为 visitarg 才能使用 Py_VISIT()

其次,我们需要提供一个清除任何可能参与循环的子对象的方法。

static int
Custom_clear(PyObject *op)
{
    CustomObject *self = (CustomObject *) op;
    Py_CLEAR(self->first);
    Py_CLEAR(self->last);
    return 0;
}

请注意 Py_CLEAR() 宏的使用。它是清除任意类型数据属性同时递减其引用计数的推荐且安全的方法。如果您在将属性设置为 NULL 之前调用 Py_XDECREF(),则属性的析构函数有可能回调到再次读取属性的代码中(尤其是 存在引用循环时)。

备注

您可以这样编写代码来模拟 Py_CLEAR()

PyObject *tmp;
tmp = self->first;
self->first = NULL;
Py_XDECREF(tmp);

然而,在删除属性时始终使用 Py_CLEAR() 要容易得多,也更不容易出错。不要为了微优化而牺牲健壮性!

解分配器 Custom_dealloc 在清除属性时可能会调用任意代码。这意味着循环 GC 可以在函数内部触发。由于 GC 假定引用计数不为零,我们需要通过调用 PyObject_GC_UnTrack() 在清除成员之前取消跟踪 GC 中的对象。以下是我们使用 PyObject_GC_UnTrack()Custom_clear 重新实现的解分配器。

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 处理程序,我们需要为循环垃圾回收修改它们。大多数扩展会使用自动提供的版本。

2.5. 子类化其他类型

可以创建从现有类型派生出来的新扩展类型。从内置类型继承最容易,因为扩展可以轻松使用它需要的 PyTypeObject。在扩展模块之间共享这些 PyTypeObject 结构可能很困难。

在此示例中,我们将创建一个继承自内置 list 类型的 SubList 类型。新类型将与普通列表完全兼容,但会有一个额外的 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,
};

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;
}

static PyModuleDef_Slot sublist_module_slots[] = {
    {Py_mod_exec, sublist_module_exec},
    {Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
    {0, NULL}
};

static PyModuleDef sublist_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "sublist",
    .m_doc = "Example module that creates an extension type.",
    .m_size = 0,
    .m_slots = sublist_module_slots,
};

PyMODINIT_FUNC
PyInit_sublist(void)
{
    return PyModuleDef_Init(&sublist_module);
}

如您所见,源代码与前面章节的 Custom 示例非常相似。我们将分解它们之间的主要区别。

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__() 方法。

在编写具有自定义 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 示例相同。

脚注