2. 定义扩展类型:教程¶
Python 允许 C 扩展模块的编写者定义新的类型,这些类型可以像内置的 str
和 list
类型一样,在 Python 代码中进行操作。所有扩展类型的代码都遵循一种模式,但有一些细节需要您在开始之前理解。本文档是对该主题的简要介绍。
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 PyModuleDef custommodule = {
.m_base = 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;
}
现在,一次性要吸收的东西太多了,但希望一些部分会从上一章中看起来很熟悉。此文件定义了三件事
Custom
对象包含什么:这是CustomObject
结构,它为每个Custom
实例分配一次。Custom
类型的行为方式:这是CustomType
结构,它定义了一组标志和函数指针,解释器在请求特定操作时会检查这些标志和函数指针。如何初始化
custom
模块:这是PyInit_custom
函数和相关的custommodule
结构。
第一部分是
typedef struct {
PyObject_HEAD
} CustomObject;
这就是 Custom 对象将包含的内容。 PyObject_HEAD
是每个对象结构开头必须的,它定义了一个名为 ob_base
的字段,类型为 PyObject
,包含指向类型对象的指针和引用计数(这些可以使用宏 Py_TYPE
和 Py_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.h
中 PyTypeObject
的实际定义比上面的定义有更多 字段。剩余的字段将由 C 编译器用零填充,通常的做法是除非你需要它们,否则不要显式指定它们。
我们将逐个字段地分析它。
.ob_base = PyVarObject_HEAD_INIT(NULL, 0)
这行是必须的模板,用于初始化上面提到的 ob_base
字段。
.tp_name = "custom.Custom",
我们类型的名称。这将出现在我们对象的默认文本表示中,以及一些错误消息中,例如
>>> "" + custom.Custom()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "custom.Custom") to str
注意,名称是一个点分隔的名称,包括模块名称和模块中的类型名称。本例中的模块是 custom
,类型是 Custom
,因此我们将类型名称设置为 custom.Custom
。使用真实的点分隔导入路径对于使你的类型与 pydoc
和 pickle
模块兼容非常重要。
.tp_basicsize = sizeof(CustomObject),
.tp_itemsize = 0,
这样 Python 就可以知道在创建新的 Custom
实例时要分配多少内存。 tp_itemsize
仅用于可变大小的对象,否则应为零。
注意
如果你希望你的类型可以从 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
类型,将许多成员填充到相应的默认值,包括 ob_type
,我们最初将其设置为 NULL
。
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
的文件中,
[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
的文件中;然后在 shell 中输入
$ python -m pip install .
应该会在子目录中生成一个名为 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(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;
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(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 = {
.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 = (initproc) Custom_init,
.tp_dealloc = (destructor) Custom_dealloc,
.tp_members = Custom_members,
.tp_methods = Custom_methods,
};
static PyModuleDef custommodule = {
.m_base =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;
if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
Py_DECREF(m);
return NULL;
}
return m;
}
此版本的模块有一些更改。
Custom
类型现在在其 C 结构中具有三个数据属性,first、last 和 number。first 和 last 变量是包含姓氏和名字的 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 *
参数。否则,编译器将发出警告。这是面向对象的 polymorphism,在 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
处理程序将 first
和 last
属性初始化为非 NULL
默认值。
tp_new
传递了要实例化的类型(如果实例化了子类,则不一定是 CustomType
)以及在调用类型时传递的任何参数,并期望返回创建的实例。 tp_new
处理程序始终接受位置参数和关键字参数,但它们通常会忽略这些参数,将参数处理留给初始化程序(也称为 C 中的 tp_init
或 Python 中的 __init__
)方法。
注意
tp_new
不应该显式调用 tp_init
,因为解释器会自行执行。
tp_new
实现调用 tp_alloc
槽来分配内存
self = (CustomObject *) type->tp_alloc(type, 0);
由于内存分配可能会失败,因此我们必须在继续之前检查 tp_alloc
结果是否为 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 时;
当在不支持循环垃圾收集的类型的
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(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)
请注意,我们必须检查 first
和 last
成员是否为 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 setuptools import Extension, setup
setup(ext_modules=[
Extension("custom", ["custom.c"]),
Extension("custom2", ["custom2.c"]),
])
然后我们重新安装,以便我们可以 import custom2
$ python -m pip install .
2.3. 提供对数据属性的更精细控制¶
在本节中,我们将提供更精细的控制,以了解如何在 Custom
示例中设置 first
和 last
属性。在我们模块的先前版本中,实例变量 first
和 last
可以设置为非字符串值,甚至可以删除。我们希望确保这些属性始终包含字符串。
#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(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;
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(CustomObject *self, void *closure)
{
return Py_NewRef(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_SETREF(self->first, Py_NewRef(value));
return 0;
}
static PyObject *
Custom_getlast(CustomObject *self, void *closure)
{
return Py_NewRef(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_SETREF(self->last, Py_NewRef(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 = {
.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 = (initproc) Custom_init,
.tp_dealloc = (destructor) Custom_dealloc,
.tp_members = Custom_members,
.tp_methods = Custom_methods,
.tp_getset = Custom_getsetters,
};
static PyModuleDef custommodule = {
.m_base = 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;
if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
Py_DECREF(m);
return NULL;
}
return m;
}
为了提供对 first
和 last
属性的更多控制,我们将使用自定义的 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", Py_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;
}
通过这些更改,我们可以确保 first
和 last
成员永远不会是 NULL
,因此我们可以在几乎所有情况下删除对 NULL
值的检查。这意味着大多数 Py_XDECREF()
调用可以转换为 Py_DECREF()
调用。我们唯一不能更改这些调用的地方是在 tp_dealloc
实现中,因为在 tp_new
中,这些成员的初始化可能会失败。
我们还像以前一样重命名模块初始化函数和模块名称,并在 setup.py
文件中添加额外的定义。
2.4. 支持循环垃圾收集¶
Python 拥有一个 循环垃圾收集器 (GC),它可以识别不需要的对象,即使它们的引用计数不为零。当对象参与循环时,就会发生这种情况。例如,考虑
>>> l = []
>>> l.append(l)
>>> del l
在这个例子中,我们创建了一个包含自身的列表。当我们删除它时,它仍然有来自自身的引用。它的引用计数不会降到零。幸运的是,Python 的循环垃圾收集器最终会发现该列表是垃圾并释放它。
在 Custom
例子的第二个版本中,我们允许任何类型的对象存储在 first
或 last
属性中 [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(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;
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(CustomObject *self, void *closure)
{
return Py_NewRef(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_XSETREF(self->first, Py_NewRef(value));
return 0;
}
static PyObject *
Custom_getlast(CustomObject *self, void *closure)
{
return Py_NewRef(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_XSETREF(self->last, Py_NewRef(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 = {
.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 = (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 = {
.m_base = 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;
if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
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
实现必须将它的参数命名为 visit 和 arg,以便使用 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);
但是,在删除属性时始终使用 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_alloc
或 tp_free
处理程序,我们需要修改它们以进行循环垃圾回收。大多数扩展将使用自动提供的版本。
2.5. 子类化其他类型¶
可以创建从现有类型派生的新扩展类型。从内置类型继承是最容易的,因为扩展可以轻松地使用它需要的 PyTypeObject
。在扩展模块之间共享这些 PyTypeObject
结构可能很困难。
在这个例子中,我们将创建一个 SubList
类型,它继承自内置的 list
类型。新类型将与普通列表完全兼容,但将有一个额外的 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_new
和 tp_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
示例相同。
脚注