2. 定义扩展类型:教程¶
Python 允许 C 扩展模块的编写者定义可以从 Python 代码操作的新类型,就像内置的 str
和 list
类型一样。所有扩展类型的代码都遵循一个模式,但在开始之前,你需要了解一些细节。本文档是对该主题的温和介绍。
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;
if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
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
类型,将多个成员填充为适当的默认值,包括我们最初设置为 NULL
的 ob_type
。
if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
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
的文件中;然后键入
$ python -m pip install .
在 shell 中应该会在一个子目录中生成一个文件 custom.so
并安装它;现在启动 Python —— 你应该能够 import custom
并使用 Custom
对象。
这不难,是吧?
当然,当前的 Custom 类型相当无趣。它没有数据,也没有任何功能。它甚至不能被子类化。
2.2. 向基本示例添加数据和方法¶
让我们扩展基本示例,添加一些数据和方法。同时,也让该类型可以作为基类使用。我们将创建一个新的模块 custom2
,它会添加这些功能。
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stddef.h> /* for offsetof() */
typedef struct {
PyObject_HEAD
PyObject *first; /* first name */
PyObject *last; /* last name */
int number;
} CustomObject;
static void
Custom_dealloc(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
中途失败,可能会发生这种情况)。然后,它调用对象类型(由 Py_TYPE(self)
计算)的 tp_free
成员来释放对象的内存。请注意,对象的类型可能不是 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
处理程序将 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
),您必须不要尝试在运行时使用方法解析顺序来确定要调用的方法。始终静态地确定您要调用的类型,并直接调用其 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
对象和一个“闭包”,它是一个 void 指针。在这种情况下,闭包被忽略。(闭包支持高级用法,其中定义数据被传递给 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()
宏,用于自动调用 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
可能会调用任意代码。这意味着可以在函数内部触发循环垃圾回收。由于垃圾回收机制假设引用计数不为零,因此我们需要在清除成员之前,通过调用 PyObject_GC_UnTrack()
从垃圾回收机制中取消对对象的跟踪。以下是使用 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
结构可能会很困难。
在此示例中,我们将创建一个从内置 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;
if (PyModule_AddObjectRef(m, "SubList", (PyObject *) &SubListType) < 0) {
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;
if (PyModule_AddObjectRef(m, "SubList", (PyObject *) &SubListType) < 0) {
Py_DECREF(m);
return NULL;
}
return m;
}
在调用 PyType_Ready()
之前,类型结构必须填充 tp_base
插槽。当我们派生现有类型时,不需要使用 PyType_GenericNew()
来填充 tp_alloc
插槽——将继承基类型的分配函数。
之后,调用 PyType_Ready()
并将类型对象添加到模块与基本的 Custom
示例相同。
脚注