2. 自定义扩展类型:教程

Python 允许编写 C 扩展模块定义可以从 Python 代码中操纵的新类型,这很像内置的 strlist 类型。所有扩展类型的代码都遵循一个模式,但是在您开始之前,您需要了解一些细节。这份文件是对这个主题介绍。

2.1. 基础

CPython 运行时将所有 Python 对象都视为类型 PyObject* 的变量,即所有 Python 对象的"基础类型"。 PyObject 结构体本身包含了对象的 reference count 和对象的"类型对象"。 类型对象确定解释器需要调用哪些 (C) 函数,例如一个属性查询一个对象,一个方法调用,或者与另一个对象相乘。 这些 C 函数被称为“类型方法”。

所以,如果你想要定义新的扩展类型,需要创建新的类型对象。

这类事情只能用例子解释,这里用一个最小化但完整的的模块,定义了新的类型叫做 Custom 在C扩展模块 custom 里。

注解

这里展示的方法是定义 static 扩展类型的传统方法。可以适合大部分用途。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 = {
    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 PyModuleDef custommodule = {
    PyModuleDef_HEAD_INIT,
    .m_name = "custom",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};

PyMODINIT_FUNC
PyInit_custom(void)
{
    PyObject *m;
    if (PyType_Ready(&CustomType) < 0)
        return NULL;

    m = PyModule_Create(&custommodule);
    if (m == NULL)
        return NULL;

    Py_INCREF(&CustomType);
    if (PyModule_AddObject(m, "Custom", (PyObject *) &CustomType) < 0) {
        Py_DECREF(&CustomType);
        Py_DECREF(m);
        return NULL;
    }

    return m;
}

这部分很容易理解,这是为了跟上一章能对接上。这个文件定义了三件事:

  1. Custom 类的对象 object 包含了: CustomObject 结构,这会为每个 Custom 实例分配一次。

  2. Custom type 的行为:这是 CustomType 结构体,其定义了一堆标识和函数指针,会指向解释器里请求的操作。

  3. 初始化 custom 模块: PyInit_custom 函数和对应的 custommodule 结构体。

结构的第一块是

typedef struct {
    PyObject_HEAD
} CustomObject;

This is what a Custom object will contain. PyObject_HEAD is mandatory at the start of each object struct and defines a field called ob_base of type PyObject, containing a pointer to a type object and a reference count (these can be accessed using the macros Py_TYPE and Py_REFCNT respectively). The reason for the macro is to abstract away the layout and to enable additional fields in debug builds.

注解

注意在宏 PyObject_HEAD 后没有分号。意外添加分号会导致编译器提示出错。

当然,对象除了在 PyObject_HEAD 存储数据外,还有额外数据;例如,如下定义了标准的Python浮点数:

typedef struct {
    PyObject_HEAD
    double ob_fval;
} PyFloatObject;

第二个位是类型对象的定义:

static PyTypeObject CustomType = {
    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.h 中实际定义的 PyTypeObject 具有比如上定义更多的 字段。 剩余的字段会由 C 编译器用零来填充,通常的做法是不显式地指定它们,除非你确实需要它们。

我们先挑选一部分,每次一个字段:

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,

该文件的其他部分应该都很容易理解,除了 PyInit_custom() 中的某些代码:

if (PyType_Ready(&CustomType) < 0)
    return;

这将初始化 Custom 类型,为一部分成员填充适当的默认值,包括我们在初始时设为 NULLob_type

Py_INCREF(&CustomType);
if (PyModule_AddObject(m, "Custom", (PyObject *) &CustomType) < 0) {
    Py_DECREF(&CustomType);
    Py_DECREF(m);
    return NULL;
}

这将把类型添加到模块字典。 这使我们能通过调用 Custom 类来创建 Custom 实例:

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

好了! 接下来要做的就是编译它;将上述代码放到名为 custom.c 的文件中然后:

from distutils.core import setup, Extension
setup(name="custom", version="1.0",
      ext_modules=[Extension("custom", ["custom.c"])])

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

$ python setup.py build

到 shell 中应当会在一个子目录中产生文件 custom.so;进入该目录并运行 Python --- 你应当能够执行 import custom 并尝试使用 Custom 对象。

这并不难,对吗?

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

注解

虽然本文档演示了用于构建 C 扩展的标准 distutils 模块,但在真实场景中还是推荐使用更新且维护得更好的 setuptools 库。 有关其用法的文档超出了本文档的范围,可以访问 Python 打包用户指南 来获取。

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

让我们通过添加一些数据和方法来扩展这个基本示例。 让我们再使原类型可以作为基类使用。 我们将创建一个新模块 custom2 来添加这些功能:

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include "structmember.h"

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

static void
Custom_dealloc(CustomObject *self)
{
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free((PyObject *) 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 = 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;
}

static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    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;
}

static PyMemberDef Custom_members[] = {
    {"first", T_OBJECT_EX, offsetof(CustomObject, first), 0,
     "first name"},
    {"last", T_OBJECT_EX, offsetof(CustomObject, last), 0,
     "last name"},
    {"number", T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};

static PyObject *
Custom_name(CustomObject *self, PyObject *Py_UNUSED(ignored))
{
    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", (PyCFunction) Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* Sentinel */
};

static PyTypeObject CustomType = {
    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 = (initproc) Custom_init,
    .tp_dealloc = (destructor) Custom_dealloc,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
};

static PyModuleDef custommodule = {
    PyModuleDef_HEAD_INIT,
    .m_name = "custom2",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};

PyMODINIT_FUNC
PyInit_custom2(void)
{
    PyObject *m;
    if (PyType_Ready(&CustomType) < 0)
        return NULL;

    m = PyModule_Create(&custommodule);
    if (m == NULL)
        return NULL;

    Py_INCREF(&CustomType);
    if (PyModule_AddObject(m, "Custom", (PyObject *) &CustomType) < 0) {
        Py_DECREF(&CustomType);
        Py_DECREF(m);
        return NULL;
    }

    return m;
}

该模块的新版本包含多处修改。

我们已经添加呢一个外部导入:

#include <structmember.h>

这包括提供被我们用来处理属性的声明,正如我们稍后所描述的。

现在 Custom 类型的 C 结构体中有三个数据属性,first, lastnumber。 其中 firstlast 变量是包含头一个和末一个名称的 Python 字符串。 number 属性是一个 C 整数。

对象的结构将被相应地更新:

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

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

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

它会被赋值给 tp_dealloc 成员:

.tp_dealloc = (destructor) Custom_dealloc,

此方法会先清空两个 Python 属性的引用计数。 Py_XDECREF() 可正确地处理其参数为 NULL 的情况(这可能在 tp_new 中途失败时发生)。 随后它将调用对象类型的 tp_free 成员(通过 Py_TYPE(self) 获取对象类型)来释放对象的内存。 请注意对象类型可以不是 CustomType,因为对象可能是一个子类的实例。

注解

上面需要强制转换 destructor 是因为我们定义了 Custom_dealloc 接受一个 CustomObject * 参数,但 tp_dealloc 函数指针预期接受一个 PyObject * 参数。 如果不这样做,编译器将发出警告。 这是 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(CustomObject *self, PyObject *args, PyObject *kwds)
{
    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 = (initproc) 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 既不会释放 GIL 也不会导致任何对我们的类型的代码的回调的时候;

  • 当减少一个 tp_dealloc 处理句柄内不支持循环垃圾回收的类型的引用计数的时候 2.

我们可能会想将我们的实例变量暴露为属性。 有几种方式可以做到这一点。 最简单的方式是定义成员的定义:

static PyMemberDef Custom_members[] = {
    {"first", T_OBJECT_EX, offsetof(CustomObject, first), 0,
     "first name"},
    {"last", T_OBJECT_EX, offsetof(CustomObject, last), 0,
     "last name"},
    {"number", T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};

并将定义放置到 tp_members 槽位中:

.tp_members = Custom_members,

每个成员的定义都有成员名称、类型、偏移量、访问旗标和文档字符串。 请参阅下面的 泛型属性管理 小节来了解详情。section below for details.

此方式的缺点之一是它没有提供限制可被赋值给 Python 属性的对象类型的办法。 我们预期 first 和 last 的名称为字符串,但它们可以被赋值为任意 Python 对象。 此外,这些属性还可以被删除,并将 C 指针设为 NULL。 即使我们可以保证这些成员被初始化为非 NULL 值,如果这些属性被删除这些成员仍可被设为 NULL

我们定义了一个单独的方法,Custom.name(),它将对象名称输出为 first 和 last 名称的拼接。

static PyObject *
Custom_name(CustomObject *self, PyObject *Py_UNUSED(ignored))
{
    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);
}

该方法的实现形式是一个接受 Custom (或 Custom 的子类) 实例作为第一个参数的 C 函数。 方法总是接受一个实例作为第一个参数。 方法也总是接受位置和关键字参数,但在本例中我们未接受任何参数也不需要接受位置参数元组或关键字参数字典。 该方法等价于以下 Python 方法:

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

请注意我们必须检查我们的 firstlast 成员是否可能为 NULL。 这是因为它们可以被删除,在此情况下它们会被设为 NULL。 更好的做法是防止删除这些属性并将属性的值限制为字符串。 我们将在下一节了解如何做到这一点。

现在我们已经定义好了方法,我们需要创建一个方法定义数组:

static PyMethodDef Custom_methods[] = {
    {"name", (PyCFunction) 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 distutils.core import setup, Extension
setup(name="custom", version="1.0",
      ext_modules=[
         Extension("custom", ["custom.c"]),
         Extension("custom2", ["custom2.c"]),
         ])

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

在本小节中,我们将对 Custom 示例中 firstlast 属性的设置提供更细致的控制。 在我们上一版本的模块中,实例变量 firstlast 可以被设为非字符串值甚至被删除。 我们希望这些属性总是包含字符串。

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include "structmember.h"

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

static void
Custom_dealloc(CustomObject *self)
{
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free((PyObject *) 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 = 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;
}

static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    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;
}

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

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

static int
Custom_setfirst(CustomObject *self, PyObject *value, void *closure)
{
    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;
}

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

static int
Custom_setlast(CustomObject *self, PyObject *value, void *closure)
{
    PyObject *tmp;
    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;
    }
    tmp = self->last;
    Py_INCREF(value);
    self->last = value;
    Py_DECREF(tmp);
    return 0;
}

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

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

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

static PyTypeObject CustomType = {
    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 = (initproc) Custom_init,
    .tp_dealloc = (destructor) Custom_dealloc,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
    .tp_getset = Custom_getsetters,
};

static PyModuleDef custommodule = {
    PyModuleDef_HEAD_INIT,
    .m_name = "custom3",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};

PyMODINIT_FUNC
PyInit_custom3(void)
{
    PyObject *m;
    if (PyType_Ready(&CustomType) < 0)
        return NULL;

    m = PyModule_Create(&custommodule);
    if (m == NULL)
        return NULL;

    Py_INCREF(&CustomType);
    if (PyModule_AddObject(m, "Custom", (PyObject *) &CustomType) < 0) {
        Py_DECREF(&CustomType);
        Py_DECREF(m);
        return NULL;
    }

    return m;
}

为了提供对于 firstlast 属性更强的控制,我们将使用自定义的读取和设置函数。 下面就是用于读取和设置 first 属性的函数:

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

static int
Custom_setfirst(CustomObject *self, PyObject *value, void *closure)
{
    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;
}

读取函数接受一个 Custom 对象和一个“闭包”,它是一个空指针。 在本例中,该闭包会被忽略。 (该闭包支持将定义数据传给读取器和设置器的进阶用法。 例如,这可以被用来允许单组读取和设置函数根据闭包中的数据来决定要读取或设置的属性。)

设置器函数将接受 Custom 对象、新值以及闭包。 新值可以为 NULL,在此情况下属性将被删除。 在我们的设置器中,如果属性被删除或者如果它的新值不是字符串则我们将引发一个错误。

我们创建一个 PyGetSetDef 结构体的数组:

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

并在 tp_getset 槽位中注册它:

.tp_getset = Custom_getsetters,

PyGetSetDef 结构体中的最后一项是上面提到的“闭包”。 在本例中,我们没有使用闭包,因此我们只传入 NULL

我们还移除了这些属性的成员定义:

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

我们还需要将 tp_init 处理句柄更新为只允许传入字符串 3:

static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    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 "structmember.h"

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

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

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

static void
Custom_dealloc(CustomObject *self)
{
    PyObject_GC_UnTrack(self);
    Custom_clear(self);
    Py_TYPE(self)->tp_free((PyObject *) 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 = 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;
}

static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    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;
}

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

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

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

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

static int
Custom_setlast(CustomObject *self, PyObject *value, void *closure)
{
    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_INCREF(value);
    Py_CLEAR(self->last);
    self->last = value;
    return 0;
}

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

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

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

static PyTypeObject CustomType = {
    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 = (initproc) Custom_init,
    .tp_dealloc = (destructor) Custom_dealloc,
    .tp_traverse = (traverseproc) Custom_traverse,
    .tp_clear = (inquiry) Custom_clear,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
    .tp_getset = Custom_getsetters,
};

static PyModuleDef custommodule = {
    PyModuleDef_HEAD_INIT,
    .m_name = "custom4",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};

PyMODINIT_FUNC
PyInit_custom4(void)
{
    PyObject *m;
    if (PyType_Ready(&CustomType) < 0)
        return NULL;

    m = PyModule_Create(&custommodule);
    if (m == NULL)
        return NULL;

    Py_INCREF(&CustomType);
    if (PyModule_AddObject(m, "Custom", (PyObject *) &CustomType) < 0) {
        Py_DECREF(&CustomType);
        Py_DECREF(m);
        return NULL;
    }

    return m;
}

首先,遍历方法让循环 GC 知道能够参加循环的子对象:

static int
Custom_traverse(CustomObject *self, visitproc visit, void *arg)
{
    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 提供了一个可自动调用 visit 函数的 Py_VISIT() 宏。 使用 Py_VISIT(),我们可以最小化 Custom_traverse 中的准备工作量:

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

注解

tp_traverse 实现必须将其参数准确命名为 visitarg 以便使用 Py_VISIT()

第二,我们需要提供一个方法用来清除任何可以参加循环的子对象:

static int
Custom_clear(CustomObject *self)
{
    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);

无论如何,在删除属性时始终使用Nevertheless, it is much easier and less error-prone to always use Py_CLEAR() 都是更简单且更不易出错的。 请不要尝试以健壮性为代价的微小优化!

释放器 Custom_dealloc 可能会在清除属性时调用任意代码。 这意味着循环 GC 可以在函数内部被触发。 由于 GC 预期引用计数不为零,我们需要通过调用 PyObject_GC_UnTrack() 来让 GC 停止追踪相关的对象。 下面是我们使用 PyObject_GC_UnTrack()Custom_clear 重新实现的释放器:

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

最后,我们将 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(SubListObject *self, PyObject *unused)
{
    self->state++;
    return PyLong_FromLong(self->state);
}

static PyMethodDef SubList_methods[] = {
    {"increment", (PyCFunction) SubList_increment, METH_NOARGS,
     PyDoc_STR("increment state counter")},
    {NULL},
};

static int
SubList_init(SubListObject *self, PyObject *args, PyObject *kwds)
{
    if (PyList_Type.tp_init((PyObject *) self, args, kwds) < 0)
        return -1;
    self->state = 0;
    return 0;
}

static PyTypeObject SubListType = {
    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 = (initproc) SubList_init,
    .tp_methods = SubList_methods,
};

static PyModuleDef sublistmodule = {
    PyModuleDef_HEAD_INIT,
    .m_name = "sublist",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};

PyMODINIT_FUNC
PyInit_sublist(void)
{
    PyObject *m;
    SubListType.tp_base = &PyList_Type;
    if (PyType_Ready(&SubListType) < 0)
        return NULL;

    m = PyModule_Create(&sublistmodule);
    if (m == NULL)
        return NULL;

    Py_INCREF(&SubListType);
    if (PyModule_AddObject(m, "SubList", (PyObject *) &SubListType) < 0) {
        Py_DECREF(&SubListType);
        Py_DECREF(m);
        return NULL;
    }

    return m;
}

如你所见,此源代码与之前小节中的 Custom 示例非常相似。 我们将逐一讲解它们之间的主要区别。

typedef struct {
    PyListObject list;
    int state;
} SubListObject;

派生类型对象的主要差异在于基类型的对象结构体必须是第一个值。 基类型将已经在其结构体的开头包括了 PyObject_HEAD()

当一个 Python 对象是 SubList 的实例时,它的 PyObject * 指针可以被安全地强制转换为 PyListObject *SubListObject *:

static int
SubList_init(SubListObject *self, PyObject *args, PyObject *kwds)
{
    if (PyList_Type.tp_init((PyObject *) self, 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 的引用来直接填充该字段;它应当随后在模块初始化函数中完成:

PyMODINIT_FUNC
PyInit_sublist(void)
{
    PyObject* m;
    SubListType.tp_base = &PyList_Type;
    if (PyType_Ready(&SubListType) < 0)
        return NULL;

    m = PyModule_Create(&sublistmodule);
    if (m == NULL)
        return NULL;

    Py_INCREF(&SubListType);
    if (PyModule_AddObject(m, "SubList", (PyObject *) &SubListType) < 0) {
        Py_DECREF(&SubListType);
        Py_DECREF(m);
        return NULL;
    }

    return m;
}

在调用 PyType_Ready() 之前,类型结构体必须已经填充 tp_base 槽位。 当我们从现有类型派生时,它不需要将 tp_alloc 槽位填充为 PyType_GenericNew() -- 来自基类型的分配函数将会被继承。

在那之后,调用 PyType_Ready() 并向模块添加类型对象是与基本的 Custom 示例一样的。

备注

1

当我们知道该对象属于基本类型,如字符串或浮点数时情况就是如此。

2

在本示例中我们需要 tp_dealloc 处理句柄中的这一机制,因为我们的类型不支持垃圾回收。

3

现在我们知道 first 和 last 成员都是字符串,因此也许我们可以对减少它们的引用计数不必太过小心,但是,我们还接受字符串子类的实例。 即使释放普通字符串不会对我们的对象执行回调,我们也不能保证释放一个字符串子类的实例不会对我们的对象执行回调。

4

而且,即使是将我们的属性限制为字符串实例,用户还是可以传入任意 str 子类因而仍能造成引用循环。