2. 定义扩展类型:教程 — Python 文档

来自菜鸟教程
Python/docs/3.8/extending/newtypes tutorial
跳转至:导航、​搜索

2. 定义扩展类型:教程

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

2.1. 基础知识

CPython 运行时将所有 Python 对象视为 PyObject* 类型的变量,作为所有 Python 对象的“基本类型”。 PyObject 结构本身只包含对象的 引用计数 和一个指向对象“类型对象”的指针。 这就是行动所在; 类型对象确定解释器调用哪些 (C) 函数,例如,在对象上查找属性、调用方法或将其与另一个对象相乘。 这些 C 函数称为“类型方法”。

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

这种事情只能通过例子来解释,所以这里有一个最小但完整的模块,它在 C 扩展模块 custom 中定义了一个名为 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 = "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;

这是自定义对象将包含的内容。 PyObject_HEAD 在每个对象结构的开头是强制性的,它定义了一个名为 ob_base 的字段,类型为 PyObject,包含一个指向类型对象的指针和一个引用计数(这些可以分别使用宏 Py_REFCNTPy_TYPE 访问)。 使用宏的原因是抽象布局并在调试版本中启用其他字段。

笔记

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 = "Custom objects",
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_new = PyType_GenericNew,
};

笔记

我们建议使用上述 C99 风格的指定初始值设定项,以避免列出您不关心的所有 PyTypeObject 字段,也避免关心字段的声明顺序。


object.hPyTypeObject的实际定义比上面的定义有更多的字段。 其余字段将由 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 = "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 并使用自定义对象。

那没那么难,是吗?

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

笔记

虽然本文档展示了用于构建 C 扩展的标准 distutils 模块,但建议在实际用例中使用更新且维护更好的 setuptools 库。 有关如何执行此操作的文档超出了本文档的范围,可以在 Python 打包用户指南 中找到。


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

让我们扩展基本示例以添加一些数据和方法。 让我们也使该类型可用作基类。 我们将创建一个新模块 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 = "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 结构中具有三个数据属性,firstlastnumberfirstlast 变量是包含名字和姓氏的 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);

由于内存分配可能会失败,因此在继续之前,我们必须根据 NULL 检查 tp_alloc 结果。

笔记

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


笔记

如果您正在创建一个合作社 tp_new(一个调用基本类型的 tp_new__new__()),您必须 not 尝试确定在运行时使用方法解析顺序调用什么方法。 始终静态地确定您要调用的类型,并直接调用其 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 也不会导致任何回调到我们类型的代码中时;
  • 在不支持循环垃圾回收 2 的类型的 tp_dealloc 处理程序中递减引用计数时。

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

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,

每个成员定义都有一个成员名称、类型、偏移量、访问标志和文档字符串。 有关详细信息,请参阅下面的 通用属性管理 部分。

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

我们定义了一个方法,Custom.name(),它将对象名称输出为名字和姓氏的串联。

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

该方法被实现为一个 C 函数,它采用 Custom(或 Custom 子类)实例作为第一个参数。 方法总是将一个实例作为第一个参数。 方法通常也接受位置参数和关键字参数,但在这种情况下,我们不接受任何参数,也不需要接受位置参数元组或关键字参数字典。 此方法等效于 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 = "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 属性,我们将使用自定义的 getter 和 setter 函数。 以下是获取和设置 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;
}

getter 函数被传递一个 Custom 对象和一个“闭包”,它是一个空指针。 在这种情况下,闭包将被忽略。 (闭包支持将定义数据传递给 getter 和 setter 的高级用法。 例如,这可以用于允许一组 getter 和 setter 函数根据闭包中的数据决定要获取或设置的属性。)

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

我们创建一个 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

为了允许循环 GC 正确检测和收集参与引用循环的 Custom 实例,我们的 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 = "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 提供了一个 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(),则该属性的析构函数可能会回调到再次读取该属性的代码([X212X ] 尤其是 如果有参考循环)。

笔记

您可以通过编写以下内容来模拟 Py_CLEAR()

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

尽管如此,在删除属性时始终使用 Py_CLEAR() 更容易且不易出错。 不要试图以牺牲健壮性为代价进行微观优化!


清除属性时,deallocator 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 = "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 槽。 当我们派生一个现有类型时,没有必要用 PyType_GenericNew() 填充 tp_alloc 槽——将继承基类型的分配函数。

之后,调用 PyType_Ready() 并将类型对象添加到模块与基本的 Custom 示例相同。

脚注

1
当我们知道对象是一个基本类型时,这是真的,比如字符串或浮点数。
2
在本例中,我们在 tp_dealloc 处理程序中依赖于此,因为我们的类型不支持垃圾收集。
3
我们现在知道第一个和最后一个成员是字符串,所以也许我们可以在减少它们的引用计数时不那么小心,但是,我们接受字符串子类的实例。 即使释放普通字符串不会回调到我们的对象中,我们也不能保证释放字符串子类的实例不会回调到我们的对象中。
4
此外,即使我们的属性仅限于字符串实例,用户也可以传递任意 str 子类,因此仍然创建引用循环。