2. 自定义扩展类型:教程¶
Python 允许编写 C 扩展模块定义可以从 Python 代码中操纵的新类型,这很像内置的 str
和 list
类型。所有扩展类型的代码都遵循一个模式,但是在您开始之前,您需要了解一些细节。这份文件是对这个主题介绍。
2.1. 基础¶
CPython 运行时会将所有 Python 对象都视为 PyObject* 类型的变量,这是所有 Python 对象的“基础类型”。 PyObject
结构体本身只包含对象的 reference count 和指向对象的“类型对象”的指针。 这是动作所针对的目标。 类型对象决定解释器要调用哪些 (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 = {
.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;
这就是一个自定义对象将会包含的内容。 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
对象。
这并不难,对吗?
当然,当前的自定义类型非常无趣。它没有数据,也不做任何事情。它甚至不能被子类化。
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 *
参数。 如果不这样做,编译器将发出警告。 这是 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,
每个成员的定义都有成员名称、类型、偏移量、访问旗标和文档字符串。 请参阅下面的 泛型属性管理 小节来了解详情。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
(或:class:!Custom 的子类) 实例作为第一个参数的 C 函数。 方法总是接受一个实例作为第一个参数。 方法往往也接受位置和关键字参数,但在本例中我们未接受任何参数也不需要接受位置参数元组或关键字参数字典。 该方法等价于以下 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
属性,我们将使用自定义的读取器和设置器函数。 以下就是用于读取和设置 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", 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 提供了一个可自动调用 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
实现必须将其参数准确命名为 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);
无论如何,在删除属性时始终使用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_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
槽位。 当我们从现有类型派生时,它不需要将 tp_alloc
槽位填充为 PyType_GenericNew()
-- 来自基类型的分配函数将会被继承。
在那之后,调用 PyType_Ready()
并将类型对象添加到模块中的过程与基本的 Custom
示例是一样的。
脚注