2. Defining New Types¶
如最后一章所述,Python允许扩展模块的编写者定义可以从Python代码操作的新类型,就像Python核心中的字符串和列表一样。
这不难;所有扩展类型的代码都遵循一个模式,但在开始之前,您需要了解一些详细信息。
2.1. The Basics¶
Python运行时将所有Python对象视为PyObject*
类型的变量,该对象充当所有Python对象的“基本类型”。PyObject
本身只包含引用计数和指向对象的“类型对象”的指针。这是行动的地方;类型对象确定哪个(C)函数被调用时,对于实例,一个属性被查找一个对象或者它乘以另一个对象。这些C函数称为“类型方法”。
所以,如果你想定义一个新的对象类型,你需要创建一个新的类型对象。
这种事情只能通过例子来解释,所以这里是一个最小的,但完整的模块,定义一个新的类型:
#include <Python.h>
typedef struct {
PyObject_HEAD
/* Type-specific fields go here. */
} noddy_NoddyObject;
static PyTypeObject noddy_NoddyType = {
PyVarObject_HEAD_INIT(NULL, 0)
"noddy.Noddy", /* tp_name */
sizeof(noddy_NoddyObject), /* tp_basicsize */
0, /* tp_itemsize */
0, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
0, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT, /* tp_flags */
"Noddy objects", /* tp_doc */
};
static PyModuleDef noddymodule = {
PyModuleDef_HEAD_INIT,
"noddy",
"Example module that creates an extension type.",
-1,
NULL, NULL, NULL, NULL, NULL
};
PyMODINIT_FUNC
PyInit_noddy(void)
{
PyObject* m;
noddy_NoddyType.tp_new = PyType_GenericNew;
if (PyType_Ready(&noddy_NoddyType) < 0)
return NULL;
m = PyModule_Create(&noddymodule);
if (m == NULL)
return NULL;
Py_INCREF(&noddy_NoddyType);
PyModule_AddObject(m, "Noddy", (PyObject *)&noddy_NoddyType);
return m;
}
现在有一点需要接受,但希望从最后一章看起来很熟悉。
新的第一位是:
typedef struct {
PyObject_HEAD
} noddy_NoddyObject;
这是一个Noddy对象将包含的东西 - 在这种情况下,只有每个Python对象包含PyObject
类型的ob_base
字段。PyObject
依次包含ob_refcnt
字段和指向类型对象的指针。这些可以分别使用宏Py_REFCNT
和Py_TYPE
访问。这些是PyObject_HEAD
宏带来的字段。宏的原因是标准化布局并在调试版本中启用特殊调试字段。
注意,在PyObject_HEAD
宏之后没有分号;一个包含在宏定义中。注意意外添加一个;它很容易从习惯做,你的编译器可能不会抱怨,但别人的可能会!(在Windows上,MSVC称为调用此错误并拒绝编译代码。)
为了比较,让我们看看标准Python浮动的相应定义:
typedef struct {
PyObject_HEAD
double ob_fval;
} PyFloatObject;
继续,我们来到紧缩 - 类型对象。
static PyTypeObject noddy_NoddyType = {
PyVarObject_HEAD_INIT(NULL, 0)
"noddy.Noddy", /* tp_name */
sizeof(noddy_NoddyObject), /* tp_basicsize */
0, /* tp_itemsize */
0, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_as_async */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
0, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT, /* tp_flags */
"Noddy objects", /* tp_doc */
};
现在,如果你去查找PyTypeObject
在object.h
的定义,你会看到它有更多的字段上面的定义。其余字段将由C编译器填充零,并且通常的做法是不明确地指定它们,除非您需要它们。
这是非常重要的,我们将选择它的顶部更进一步:
PyVarObject_HEAD_INIT(NULL, 0)
这条线有点疣;我们想写的是:
PyVarObject_HEAD_INIT(&PyType_Type, 0)
因为类型对象的类型是“类型”,但这不是严格符合C和一些编译器抱怨。幸运的是,这个成员将由PyType_Ready()
填写。
"noddy.Noddy", /* tp_name */
我们的类型的名称。这将出现在我们的对象的默认文本表示和一些错误消息中,例如:
>>> "" + noddy.new_noddy()
Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: cannot add type "noddy.Noddy" to string
请注意,名称是一个点名称,其中包括模块名称和模块中类型的名称。在这种情况下,模块是noddy
,类型是Noddy
,因此我们将类型名称设置为noddy.Noddy
。
sizeof(noddy_NoddyObject), /* tp_basicsize */
这是为了让Python知道在调用PyObject_New()
时要分配多少内存。
注意
如果你希望你的类型是Python的子类,并且你的类型和它的基类型有相同的tp_basicsize
,你可能会遇到多重继承的问题。您的类型的Python子类必须先在其__bases__
中列出您的类型,否则将无法调用您的类型的__new__()
方法, 。您可以通过确保类型的tp_basicsize
大于其基类型的值,来避免此问题。大多数时候,这将是真的,无论如何,因为你的基本类型将是object
,否则你将添加数据成员到您的基本类型,因此增加其大小。
0, /* tp_itemsize */
这与可变长度对象(如列表和字符串)有关。现在忽略这个。
跳过一些我们不提供的类型方法,我们将类标志设置为Py_TPFLAGS_DEFAULT
。
Py_TPFLAGS_DEFAULT, /* tp_flags */
所有类型都应在其标志中包含此常量。它使所有成员定义,直到至少Python 3.3。如果你需要更多的成员,你将需要或相应的标志。
我们为tp_doc
中的类型提供一个doc字符串。
"Noddy objects", /* tp_doc */
现在我们进入类型方法,使你的对象与其他对象不同的东西。我们不会在这个版本的模块中实现这些。我们稍后将展开这个例子,以便有更有趣的行为。
现在,我们想要做的就是创建新的Noddy
对象。要启用对象创建,我们必须提供tp_new
实现。在这种情况下,我们可以使用API函数PyType_GenericNew()
提供的默认实现。我们只想将它分配给tp_new
槽,但是为了便于移植,我们不能在某些平台或编译器上静态初始化一个具有定义的函数的结构成员另一个C模块,因此,我们将在调用PyType_Ready()
之前在模块初始化函数中分配tp_new
noddy_NoddyType.tp_new = PyType_GenericNew;
if (PyType_Ready(&noddy_NoddyType) < 0)
return;
所有其他类型的方法是NULL,所以我们稍后讨论它们 - 这是后面的部分!
除了PyInit_noddy()
中的一些代码,文件中的其他所有内容都应该是熟悉的:
if (PyType_Ready(&noddy_NoddyType) < 0)
return;
这将初始化Noddy
类型,在许多成员中进行归档,包括我们最初设置为NULL的ob_type
。
PyModule_AddObject(m, "Noddy", (PyObject *)&noddy_NoddyType);
这会将类型添加到模块字典。这允许我们通过调用Noddy
类创建Noddy
实例:
>>> import noddy
>>> mynoddy = noddy.Noddy()
而已!剩下的只是建造它;将上述代码放在一个名为noddy.c
的文件中
from distutils.core import setup, Extension
setup(name="noddy", version="1.0",
ext_modules=[Extension("noddy", ["noddy.c"])])
在一个名为setup.py
的文件中;然后输入
$ python setup.py build
在shell应该在子目录中产生一个文件noddy.so
;移动到该目录并启动Python - 您应该能够导入 noddy
并使用Noddy对象进行游戏。
那不是那么难,是吗?
当然,当前Noddy类型是非常不感兴趣。它没有数据,不做任何事情。它甚至不能被子类化。
2.1.1. Adding data and methods to the Basic example¶
让我们扩展一个基本的例子来添加一些数据和方法。让我们也让类型可以作为一个基类。我们将创建一个新模块,noddy2
可添加以下功能:
#include <Python.h>
#include "structmember.h"
typedef struct {
PyObject_HEAD
PyObject *first; /* first name */
PyObject *last; /* last name */
int number;
} Noddy;
static void
Noddy_dealloc(Noddy* self)
{
Py_XDECREF(self->first);
Py_XDECREF(self->last);
Py_TYPE(self)->tp_free((PyObject*)self);
}
static PyObject *
Noddy_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
Noddy *self;
self = (Noddy *)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
Noddy_init(Noddy *self, PyObject *args, PyObject *kwds)
{
PyObject *first=NULL, *last=NULL, *tmp;
static char *kwlist[] = {"first", "last", "number", NULL};
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 Noddy_members[] = {
{"first", T_OBJECT_EX, offsetof(Noddy, first), 0,
"first name"},
{"last", T_OBJECT_EX, offsetof(Noddy, last), 0,
"last name"},
{"number", T_INT, offsetof(Noddy, number), 0,
"noddy number"},
{NULL} /* Sentinel */
};
static PyObject *
Noddy_name(Noddy* self)
{
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 Noddy_methods[] = {
{"name", (PyCFunction)Noddy_name, METH_NOARGS,
"Return the name, combining the first and last name"
},
{NULL} /* Sentinel */
};
static PyTypeObject NoddyType = {
PyVarObject_HEAD_INIT(NULL, 0)
"noddy.Noddy", /* tp_name */
sizeof(Noddy), /* tp_basicsize */
0, /* tp_itemsize */
(destructor)Noddy_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
0, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT |
Py_TPFLAGS_BASETYPE, /* tp_flags */
"Noddy objects", /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
Noddy_methods, /* tp_methods */
Noddy_members, /* tp_members */
0, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
(initproc)Noddy_init, /* tp_init */
0, /* tp_alloc */
Noddy_new, /* tp_new */
};
static PyModuleDef noddy2module = {
PyModuleDef_HEAD_INIT,
"noddy2",
"Example module that creates an extension type.",
-1,
NULL, NULL, NULL, NULL, NULL
};
PyMODINIT_FUNC
PyInit_noddy2(void)
{
PyObject* m;
if (PyType_Ready(&NoddyType) < 0)
return NULL;
m = PyModule_Create(&noddy2module);
if (m == NULL)
return NULL;
Py_INCREF(&NoddyType);
PyModule_AddObject(m, "Noddy", (PyObject *)&NoddyType);
return m;
}
此版本的模块有多个更改。
我们添加了一个额外的包括:
#include <structmember.h>
这包括提供我们用于处理属性的声明,稍后将进行描述。
Noddy
对象结构的名称已缩短到Noddy
。类型对象名称已缩短为NoddyType
。
Noddy
类型现在具有三个数据属性,第一,最后和数字。第一个和最后变量是包含名和姓的Python字符串。数字属性是一个整数。
对象结构将相应更新:
typedef struct {
PyObject_HEAD
PyObject *first;
PyObject *last;
int number;
} Noddy;
因为我们现在有数据要管理,我们必须更加小心对象分配和释放。至少,我们需要一个释放方法:
static void
Noddy_dealloc(Noddy* self)
{
Py_XDECREF(self->first);
Py_XDECREF(self->last);
Py_TYPE(self)->tp_free((PyObject*)self);
}
其分配给tp_dealloc
成员:
(destructor)Noddy_dealloc, /*tp_dealloc*/
此方法减少两个Python属性的引用计数。这里使用Py_XDECREF()
是因为first
和last
成员可以是NULL。然后调用对象类型的tp_free
成员来释放对象的内存。请注意,对象的类型可能不是NoddyType
,因为对象可能是子类的实例。
我们要确保名字和姓氏被初始化为空字符串,所以我们提供了一个新的方法:
static PyObject *
Noddy_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
Noddy *self;
self = (Noddy *)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
成员中:
Noddy_new, /* tp_new */
新成员负责创建(与初始化)类型的对象。它在Python中作为__new__()
方法公开。有关__new__()
方法的详细讨论,请参阅标题为“Unifying types and classes in Python”的文章。实现新方法的一个原因是确保实例变量的初始值。在这种情况下,我们使用新方法来确保成员first
和last
的初始值不是NULL。如果我们不关心初始值是否NULL,我们可以使用PyType_GenericNew()
作为我们的新方法,就像我们以前做的一样。PyType_GenericNew()
将所有实例变量成员初始化为NULL。
新方法是一个静态方法,它传递被实例化的类型,当调用类型时传递的任何参数,并返回创建的新对象。新方法总是接受位置和关键字参数,但它们通常忽略参数,将参数处理留给初始化方法。注意,如果类型支持子类化,传递的类型可能不是被定义的类型。新方法调用tp_alloc
插槽分配内存。我们不会填充tp_alloc
槽。相反,PyType_Ready()
通过从我们的基类继承它来填充它,它默认为object
。大多数类型使用默认分配。
注意
如果您要创建协作tp_new
(调用基本类型的tp_new
或__new__()
),则必须不尝试在运行时确定使用方法解析顺序调用哪个方法。始终静态确定要调用的类型,并直接调用其tp_new
,或通过type->tp_base->tp_new
调用它。如果不这样做,那么类型的Python子类也可能继承自其他Python定义的类,可能无法正常工作。(具体来说,您可能无法创建此类子类的实例,而无法获取TypeError
。)
我们提供了一个初始化函数:
static int
Noddy_init(Noddy *self, PyObject *args, PyObject *kwds)
{
PyObject *first=NULL, *last=NULL, *tmp;
static char *kwlist[] = {"first", "last", "number", NULL};
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
插槽。
(initproc)Noddy_init, /* tp_init */
tp_init
槽在Python中显示为__init__()
方法。它用于在对象创建后对其进行初始化。与新方法不同,我们不能保证初始化器被调用。取消对象的取消时,不会调用初始化程序,它可以被覆盖。我们的初始化程序接受参数,为我们的实例提供初始值。初始化器总是接受位置和关键字参数。初始化器应返回成功时为0或错误时为-1。
初始化程序可以多次调用。任何人都可以在我们的对象上调用__init__()
方法。为此,我们在分配新值时必须格外小心。我们可能会想,例如,像这样分配first
成员:
if (first) {
Py_XDECREF(self->first);
Py_INCREF(first);
self->first = first;
}
但这是有风险的。我们的类型不限制first
成员的类型,因此它可以是任何类型的对象。它可以有一个析构函数,导致代码被执行,试图访问first
成员。为了偏执和保护自己免受这种可能性,我们几乎总是在递减他们的引用计数之前重新分配成员。什么时候不这样做?
- 当我们绝对知道引用计数大于1
- 当我们知道对象[1]的解除分配不会导致任何回调到我们类型的代码
- 当不支持垃圾回收时,在
tp_dealloc
处理程序中减少引用计数[2]
我们希望将实例变量公开为属性。有很多方法可以做到这一点。最简单的方法是定义成员定义:
static PyMemberDef Noddy_members[] = {
{"first", T_OBJECT_EX, offsetof(Noddy, first), 0,
"first name"},
{"last", T_OBJECT_EX, offsetof(Noddy, last), 0,
"last name"},
{"number", T_INT, offsetof(Noddy, number), 0,
"noddy number"},
{NULL} /* Sentinel */
};
并将定义放在tp_members
插槽中:
Noddy_members, /* tp_members */
每个成员定义都有成员名,类型,偏移量,访问标志和文档字符串。有关详细信息,请参阅下面的Generic Attribute Management部分。
这种方法的缺点是它不提供一种方法来限制可以分配给Python属性的对象类型。我们期望的名字和姓氏是字符串,但任何Python对象可以分配。此外,可以删除属性,将C指针设置为NULL。即使我们可以确保成员被初始化为非NULL值,如果属性被删除,成员可以设置为NULL。
我们定义一个方法,name()
,输出对象名称作为第一个和最后一个名字的连接。
static PyObject *
Noddy_name(Noddy* self)
{
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函数实现,该函数以Noddy
(或Noddy
子类)实例作为第一个参数。方法总是以实例作为第一个参数。方法通常也使用位置和关键字参数,但在这种情况下,我们不采取任何,不需要接受位置参数元组或关键字参数字典。这个方法等同于Python方法:
def name(self):
return "%s %s" % (self.first, self.last)
请注意,我们必须检查我们的first
和last
成员是NULL的可能性。这是因为它们可以删除,在这种情况下,它们设置为NULL。最好是防止删除这些属性,并将属性值限制为字符串。我们将在下一节中看到如何做到这一点。
现在我们已经定义了方法,我们需要创建一个方法定义数组:
static PyMethodDef Noddy_methods[] = {
{"name", (PyCFunction)Noddy_name, METH_NOARGS,
"Return the name, combining the first and last name"
},
{NULL} /* Sentinel */
};
并将它们分配给tp_methods
插槽:
Noddy_methods, /* tp_methods */
注意,我们使用METH_NOARGS
标志来表示该方法没有传递任何参数。
最后,我们将使我们的类型可用作基类。我们已经仔细地编写了我们的方法,以便他们不对创建或使用的对象的类型做任何假设,所以我们需要做的是将Py_TPFLAGS_BASETYPE
添加到我们的类标志定义:
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/
我们将PyInit_noddy()
重命名为PyInit_noddy2()
并更新PyModuleDef
结构中的模块名称。
最后,更新我们的setup.py
文件以构建新模块:
from distutils.core import setup, Extension
setup(name="noddy", version="1.0",
ext_modules=[
Extension("noddy", ["noddy.c"]),
Extension("noddy2", ["noddy2.c"]),
])
2.1.2. Providing finer control over data attributes¶
在本节中,我们将对如何在Noddy
示例中设置first
和last
属性提供更精细的控制。在我们的模块的先前版本中,实例变量first
和last
可以设置为非字符串值,甚至删除。我们要确保这些属性总是包含字符串。
#include <Python.h>
#include "structmember.h"
typedef struct {
PyObject_HEAD
PyObject *first;
PyObject *last;
int number;
} Noddy;
static void
Noddy_dealloc(Noddy* self)
{
Py_XDECREF(self->first);
Py_XDECREF(self->last);
Py_TYPE(self)->tp_free((PyObject*)self);
}
static PyObject *
Noddy_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
Noddy *self;
self = (Noddy *)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
Noddy_init(Noddy *self, PyObject *args, PyObject *kwds)
{
PyObject *first=NULL, *last=NULL, *tmp;
static char *kwlist[] = {"first", "last", "number", NULL};
if (! PyArg_ParseTupleAndKeywords(args, kwds, "|SSi", 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 Noddy_members[] = {
{"number", T_INT, offsetof(Noddy, number), 0,
"noddy number"},
{NULL} /* Sentinel */
};
static PyObject *
Noddy_getfirst(Noddy *self, void *closure)
{
Py_INCREF(self->first);
return self->first;
}
static int
Noddy_setfirst(Noddy *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_DECREF(self->first);
Py_INCREF(value);
self->first = value;
return 0;
}
static PyObject *
Noddy_getlast(Noddy *self, void *closure)
{
Py_INCREF(self->last);
return self->last;
}
static int
Noddy_setlast(Noddy *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_DECREF(self->last);
Py_INCREF(value);
self->last = value;
return 0;
}
static PyGetSetDef Noddy_getseters[] = {
{"first",
(getter)Noddy_getfirst, (setter)Noddy_setfirst,
"first name",
NULL},
{"last",
(getter)Noddy_getlast, (setter)Noddy_setlast,
"last name",
NULL},
{NULL} /* Sentinel */
};
static PyObject *
Noddy_name(Noddy* self)
{
return PyUnicode_FromFormat("%S %S", self->first, self->last);
}
static PyMethodDef Noddy_methods[] = {
{"name", (PyCFunction)Noddy_name, METH_NOARGS,
"Return the name, combining the first and last name"
},
{NULL} /* Sentinel */
};
static PyTypeObject NoddyType = {
PyVarObject_HEAD_INIT(NULL, 0)
"noddy.Noddy", /* tp_name */
sizeof(Noddy), /* tp_basicsize */
0, /* tp_itemsize */
(destructor)Noddy_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
0, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT |
Py_TPFLAGS_BASETYPE, /* tp_flags */
"Noddy objects", /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
Noddy_methods, /* tp_methods */
Noddy_members, /* tp_members */
Noddy_getseters, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
(initproc)Noddy_init, /* tp_init */
0, /* tp_alloc */
Noddy_new, /* tp_new */
};
static PyModuleDef noddy3module = {
PyModuleDef_HEAD_INIT,
"noddy3",
"Example module that creates an extension type.",
-1,
NULL, NULL, NULL, NULL, NULL
};
PyMODINIT_FUNC
PyInit_noddy3(void)
{
PyObject* m;
if (PyType_Ready(&NoddyType) < 0)
return NULL;
m = PyModule_Create(&noddy3module);
if (m == NULL)
return NULL;
Py_INCREF(&NoddyType);
PyModule_AddObject(m, "Noddy", (PyObject *)&NoddyType);
return m;
}
为了提供更好的控制,在first
和last
属性上,我们将使用自定义的getter和setter函数。以下是获取和设置first
属性的函数:
Noddy_getfirst(Noddy *self, void *closure)
{
Py_INCREF(self->first);
return self->first;
}
static int
Noddy_setfirst(Noddy *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 str");
return -1;
}
Py_DECREF(self->first);
Py_INCREF(value);
self->first = value;
return 0;
}
getter函数传递一个Noddy
对象和一个“closure”,它是void指针。在这种情况下,忽略闭包。(闭包支持高级用法,其中定义数据被传递给getter和setter。例如,这可以用于允许基于闭包中的数据决定属性获取或设置的单组getter和setter函数。)
setter函数传递Noddy
对象,新值和闭包。新值可以是NULL,在这种情况下,属性正在被删除。在我们的setter中,如果属性被删除或者属性值不是字符串,我们引发一个错误。
我们创建一个PyGetSetDef
结构的数组:
static PyGetSetDef Noddy_getseters[] = {
{"first",
(getter)Noddy_getfirst, (setter)Noddy_setfirst,
"first name",
NULL},
{"last",
(getter)Noddy_getlast, (setter)Noddy_setlast,
"last name",
NULL},
{NULL} /* Sentinel */
};
并将其注册到tp_getset
插槽中:
Noddy_getseters, /* tp_getset */
以注册我们的属性getter和setter。
PyGetSetDef
结构中的最后一个项目是上面提到的闭包。在这种情况下,我们不使用闭包,因此我们只需传递NULL。
我们还删除这些属性的成员定义:
static PyMemberDef Noddy_members[] = {
{"number", T_INT, offsetof(Noddy, number), 0,
"noddy number"},
{NULL} /* Sentinel */
};
我们还需要更新tp_init
处理程序,以便只允许传递字符串[3]:
static int
Noddy_init(Noddy *self, PyObject *args, PyObject *kwds)
{
PyObject *first=NULL, *last=NULL, *tmp;
static char *kwlist[] = {"first", "last", "number", NULL};
if (! PyArg_ParseTupleAndKeywords(args, kwds, "|SSi", 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 t5 >值在几乎所有情况下。这意味着大多数Py_XDECREF()
调用可以转换为Py_DECREF()
调用。我们不能改变这些调用的唯一的地方是在deallocator中,这里有可能这些成员的初始化在构造函数中失败。
我们还像以前一样重命名初始化函数中的模块初始化函数和模块名,并且为setup.py
文件添加一个额外的定义。
2.1.3. Supporting cyclic garbage collection¶
Python有一个循环垃圾收集器,它可以标识不需要的对象,即使它们的引用计数不为零。这可能发生在对象涉及循环时。例如,考虑:
>>> l = []
>>> l.append(l)
>>> del l
在这个例子中,我们创建一个包含自身的列表。当我们删除它,它仍然有自己的引用。它的引用计数不会下降到零。幸运的是,Python的循环垃圾收集器最终会发现列表是垃圾并释放它。
在Noddy
示例的第二个版本中,我们允许任何类型的对象存储在first
或last
属性中。[4]这意味着Noddy
对象可以参与循环:
>>> import noddy2
>>> n = noddy2.Noddy()
>>> l = [n]
>>> n.first = l
这是很愚蠢的,但它给了我们一个借口,添加对循环垃圾收集器到Noddy
示例的支持。为了支持循环垃圾容器,类型需要填充两个槽并设置一个启用这些槽的类标志:
#include <Python.h>
#include "structmember.h"
typedef struct {
PyObject_HEAD
PyObject *first;
PyObject *last;
int number;
} Noddy;
static int
Noddy_traverse(Noddy *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;
}
static int
Noddy_clear(Noddy *self)
{
PyObject *tmp;
tmp = self->first;
self->first = NULL;
Py_XDECREF(tmp);
tmp = self->last;
self->last = NULL;
Py_XDECREF(tmp);
return 0;
}
static void
Noddy_dealloc(Noddy* self)
{
Noddy_clear(self);
Py_TYPE(self)->tp_free((PyObject*)self);
}
static PyObject *
Noddy_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
Noddy *self;
self = (Noddy *)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
Noddy_init(Noddy *self, PyObject *args, PyObject *kwds)
{
PyObject *first=NULL, *last=NULL, *tmp;
static char *kwlist[] = {"first", "last", "number", NULL};
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 Noddy_members[] = {
{"first", T_OBJECT_EX, offsetof(Noddy, first), 0,
"first name"},
{"last", T_OBJECT_EX, offsetof(Noddy, last), 0,
"last name"},
{"number", T_INT, offsetof(Noddy, number), 0,
"noddy number"},
{NULL} /* Sentinel */
};
static PyObject *
Noddy_name(Noddy* self)
{
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 Noddy_methods[] = {
{"name", (PyCFunction)Noddy_name, METH_NOARGS,
"Return the name, combining the first and last name"
},
{NULL} /* Sentinel */
};
static PyTypeObject NoddyType = {
PyVarObject_HEAD_INIT(NULL, 0)
"noddy.Noddy", /* tp_name */
sizeof(Noddy), /* tp_basicsize */
0, /* tp_itemsize */
(destructor)Noddy_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
0, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT |
Py_TPFLAGS_BASETYPE |
Py_TPFLAGS_HAVE_GC, /* tp_flags */
"Noddy objects", /* tp_doc */
(traverseproc)Noddy_traverse, /* tp_traverse */
(inquiry)Noddy_clear, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
Noddy_methods, /* tp_methods */
Noddy_members, /* tp_members */
0, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
(initproc)Noddy_init, /* tp_init */
0, /* tp_alloc */
Noddy_new, /* tp_new */
};
static PyModuleDef noddy4module = {
PyModuleDef_HEAD_INIT,
"noddy4",
"Example module that creates an extension type.",
-1,
NULL, NULL, NULL, NULL, NULL
};
PyMODINIT_FUNC
PyInit_noddy4(void)
{
PyObject* m;
if (PyType_Ready(&NoddyType) < 0)
return NULL;
m = PyModule_Create(&noddy4module);
if (m == NULL)
return NULL;
Py_INCREF(&NoddyType);
PyModule_AddObject(m, "Noddy", (PyObject *)&NoddyType);
return m;
}
遍历方法提供对可以参与循环的子对象的访问:
static int
Noddy_traverse(Noddy *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()
函数将subobject和附加参数arg作为参数传递给遍历方法。它返回一个整数值,如果它是非零,必须返回。
Python提供了一个自动调用访问函数的Py_VISIT()
宏。使用Py_VISIT()
,Noddy_traverse()
可以简化:
static int
Noddy_traverse(Noddy *self, visitproc visit, void *arg)
{
Py_VISIT(self->first);
Py_VISIT(self->last);
return 0;
}
注意
请注意,为了使用Py_VISIT()
,tp_traverse
实施必须完全命名其参数访问和arg。这是为了鼓励在这些无聊的实现中的一致性。
我们还需要提供一种清除任何可以参与循环的子对象的方法。我们实现该方法并重新实现释放器使用它:
static int
Noddy_clear(Noddy *self)
{
PyObject *tmp;
tmp = self->first;
self->first = NULL;
Py_XDECREF(tmp);
tmp = self->last;
self->last = NULL;
Py_XDECREF(tmp);
return 0;
}
static void
Noddy_dealloc(Noddy* self)
{
Noddy_clear(self);
Py_TYPE(self)->tp_free((PyObject*)self);
}
注意在Noddy_clear()
中使用临时变量。我们使用临时变量,以便我们可以在递减其引用计数之前将每个成员设置为NULL。我们这样做是因为,如前所述,如果引用计数下降到零,我们可能会导致代码运行调用回到对象。此外,因为我们现在支持垃圾容器,我们还必须担心代码运行触发垃圾容器。如果运行垃圾容器,我们的tp_traverse
处理程序可以被调用。当成员的引用计数下降到零,并且其值未设置为NULL时,我们不能有机会使用Noddy_traverse()
。
Python提供了一个Py_CLEAR()
,可以自动小心减少引用计数。使用Py_CLEAR()
,可以简化Noddy_clear()
函数:
static int
Noddy_clear(Noddy *self)
{
Py_CLEAR(self->first);
Py_CLEAR(self->last);
return 0;
}
最后,我们将Py_TPFLAGS_HAVE_GC
标志添加到类标志:
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, /* tp_flags */
这就是它。如果我们编写了自定义的tp_alloc
或tp_free
插槽,我们需要修改它们以获得循环垃圾容器。大多数扩展程序将使用自动提供的版本。
2.1.4. Subclassing other types¶
可以创建从现有类型派生的新扩展类型。从内置类型继承是最容易的,因为扩展可以很容易地使用它需要的PyTypeObject
。可能很难在扩展模块之间共享这些PyTypeObject
结构。
在这个例子中,我们将创建一个从内建list
类型继承的Shoddy
类型。新类型将与常规列表完全兼容,但将有一个额外的increment()
方法来增加内部计数器。
>>> import shoddy
>>> s = shoddy.Shoddy(range(3))
>>> s.extend(s)
>>> print(len(s))
6
>>> print(s.increment())
1
>>> print(s.increment())
2
#include <Python.h>
typedef struct {
PyListObject list;
int state;
} Shoddy;
static PyObject *
Shoddy_increment(Shoddy *self, PyObject *unused)
{
self->state++;
return PyLong_FromLong(self->state);
}
static PyMethodDef Shoddy_methods[] = {
{"increment", (PyCFunction)Shoddy_increment, METH_NOARGS,
PyDoc_STR("increment state counter")},
{NULL, NULL},
};
static int
Shoddy_init(Shoddy *self, PyObject *args, PyObject *kwds)
{
if (PyList_Type.tp_init((PyObject *)self, args, kwds) < 0)
return -1;
self->state = 0;
return 0;
}
static PyTypeObject ShoddyType = {
PyObject_HEAD_INIT(NULL)
"shoddy.Shoddy", /* tp_name */
sizeof(Shoddy), /* tp_basicsize */
0, /* tp_itemsize */
0, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
0, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT |
Py_TPFLAGS_BASETYPE, /* tp_flags */
0, /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
Shoddy_methods, /* tp_methods */
0, /* tp_members */
0, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
(initproc)Shoddy_init, /* tp_init */
0, /* tp_alloc */
0, /* tp_new */
};
static PyModuleDef shoddymodule = {
PyModuleDef_HEAD_INIT,
"shoddy",
"Shoddy module",
-1,
NULL, NULL, NULL, NULL, NULL
};
PyMODINIT_FUNC
PyInit_shoddy(void)
{
PyObject *m;
ShoddyType.tp_base = &PyList_Type;
if (PyType_Ready(&ShoddyType) < 0)
return NULL;
m = PyModule_Create(&shoddymodule);
if (m == NULL)
return NULL;
Py_INCREF(&ShoddyType);
PyModule_AddObject(m, "Shoddy", (PyObject *) &ShoddyType);
return m;
}
如您所见,源代码与上一节中的Noddy
示例非常相似。我们将分解它们之间的主要区别。
typedef struct {
PyListObject list;
int state;
} Shoddy;
派生类型对象的主要区别是基本类型的对象结构必须是第一个值。基本类型已经在其结构的开始处包括PyObject_HEAD()
。
当Python对象是Shoddy
实例时,其PyObject *指针可以安全地转换到PyListObject *和Shoddy * t4>。
static int
Shoddy_init(Shoddy *self, PyObject *args, PyObject *kwds)
{
if (PyList_Type.tp_init((PyObject *)self, args, kwds) < 0)
return -1;
self->state = 0;
return 0;
}
在我们类型的__init__
方法中,我们可以看到如何调用基类型的__init__
方法。
当使用自定义new
和dealloc
方法编写类型时,此模式很重要。new
方法不应该实际上为tp_alloc
创建对象的内存,当调用tp_new
时,将由基类处理。
当为Shoddy
类型填写PyTypeObject()
时,会看到tp_base()
的插槽。由于跨平台编译器问题,您不能直接使用PyList_Type()
填充该字段;它可以稍后在模块的init()
函数中完成。
PyMODINIT_FUNC
PyInit_shoddy(void)
{
PyObject *m;
ShoddyType.tp_base = &PyList_Type;
if (PyType_Ready(&ShoddyType) < 0)
return NULL;
m = PyModule_Create(&shoddymodule);
if (m == NULL)
return NULL;
Py_INCREF(&ShoddyType);
PyModule_AddObject(m, "Shoddy", (PyObject *) &ShoddyType);
return m;
}
在调用PyType_Ready()
之前,类型结构必须填充tp_base
插槽。当我们导出一个新类型时,没有必要用PyType_GenericNew()
填充tp_alloc
槽 - 将继承基类型的分配函数。
之后,调用PyType_Ready()
并将类型对象添加到模块中与基本的Noddy
示例相同。
2.2. Type Methods¶
本节旨在快速介绍您可以实现的各种类型方法及其操作。
这里是PyTypeObject
的定义,其中一些字段仅用于调试版本中省略:
typedef struct _typeobject {
PyObject_VAR_HEAD
const char *tp_name; /* For printing, in format "<module>.<name>" */
Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */
/* Methods to implement standard operations */
destructor tp_dealloc;
printfunc tp_print;
getattrfunc tp_getattr;
setattrfunc tp_setattr;
PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2)
or tp_reserved (Python 3) */
reprfunc tp_repr;
/* Method suites for standard classes */
PyNumberMethods *tp_as_number;
PySequenceMethods *tp_as_sequence;
PyMappingMethods *tp_as_mapping;
/* More standard operations (here for binary compatibility) */
hashfunc tp_hash;
ternaryfunc tp_call;
reprfunc tp_str;
getattrofunc tp_getattro;
setattrofunc tp_setattro;
/* Functions to access object as input/output buffer */
PyBufferProcs *tp_as_buffer;
/* Flags to define presence of optional/expanded features */
unsigned long tp_flags;
const char *tp_doc; /* Documentation string */
/* call function for all accessible objects */
traverseproc tp_traverse;
/* delete references to contained objects */
inquiry tp_clear;
/* rich comparisons */
richcmpfunc tp_richcompare;
/* weak reference enabler */
Py_ssize_t tp_weaklistoffset;
/* Iterators */
getiterfunc tp_iter;
iternextfunc tp_iternext;
/* Attribute descriptor and subclassing stuff */
struct PyMethodDef *tp_methods;
struct PyMemberDef *tp_members;
struct PyGetSetDef *tp_getset;
struct _typeobject *tp_base;
PyObject *tp_dict;
descrgetfunc tp_descr_get;
descrsetfunc tp_descr_set;
Py_ssize_t tp_dictoffset;
initproc tp_init;
allocfunc tp_alloc;
newfunc tp_new;
freefunc tp_free; /* Low-level free-memory routine */
inquiry tp_is_gc; /* For PyObject_IS_GC */
PyObject *tp_bases;
PyObject *tp_mro; /* method resolution order */
PyObject *tp_cache;
PyObject *tp_subclasses;
PyObject *tp_weaklist;
destructor tp_del;
/* Type attribute cache version tag. Added in version 2.6 */
unsigned int tp_version_tag;
destructor tp_finalize;
} PyTypeObject;
现在是很多的方法。不要担心太多,虽然 - 如果你有一个类型,你想定义,机会是非常好的,你将只实施一小部分。
正如你可能预期的那样,我们将讨论这一点,并提供关于各种处理程序的更多信息。我们不会按照它们在结构中定义的顺序进行,因为有很多影响字段排序的历史行李;确保你的类型初始化保持字段在正确的顺序!通常最容易找到一个包含您需要的所有字段的示例(即使它们初始化为0
),然后更改值以适应新类型。
const char *tp_name; /* For printing */
类型的名称 - 如上一节所述,这将出现在各种地方,几乎完全用于诊断目的。尝试选择在这种情况下将有所帮助的东西!
Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */
这些字段告诉运行时创建此类型的新对象时分配多少内存。Python对变量长度结构(认为:字符串,列表)有一些内建支持,它是tp_itemsize
字段所在的位置。这将在稍后处理。
const char *tp_doc;
在这里,您可以放置一个字符串(或其地址),当Python脚本引用obj.__doc__
时,它将返回以检索文档字符串。
现在我们来到基本的类型方法 - 大多数扩展类型将实现。
2.2.1. Finalization and De-allocation¶
destructor tp_dealloc;
当你的类型的实例的引用计数减少到零,并且Python解释器想要回收它时,调用此函数。如果你的类型有内存释放或其他清理来执行,你可以把它放在这里。对象本身也需要在这里释放。这里是这个函数的一个例子:
static void
newdatatype_dealloc(newdatatypeobject * obj)
{
free(obj->obj_UnderlyingDatatypePtr);
Py_TYPE(obj)->tp_free(obj);
}
解除分配器函数的一个重要要求是它只留下任何挂起的异常。这是很重要的,因为解释器经常被称为解释器展开Python栈;当堆栈由于异常(而不是正常返回)而解开时,没有做任何事情来保护解除分配器看不到已经设置了异常。解除分配器执行的任何可能导致执行额外的Python代码的动作可能会检测到异常已设置。这可能导致来自解释器的误导错误。防止这种情况的正确方法是在执行不安全操作之前保存待处理的异常,并在完成后恢复。这可以使用PyErr_Fetch()
和PyErr_Restore()
函数来完成:
static void
my_dealloc(PyObject *obj)
{
MyObject *self = (MyObject *) obj;
PyObject *cbresult;
if (self->my_callback != NULL) {
PyObject *err_type, *err_value, *err_traceback;
/* This saves the current exception state */
PyErr_Fetch(&err_type, &err_value, &err_traceback);
cbresult = PyObject_CallObject(self->my_callback, NULL);
if (cbresult == NULL)
PyErr_WriteUnraisable(self->my_callback);
else
Py_DECREF(cbresult);
/* This restores the saved exception state */
PyErr_Restore(err_type, err_value, err_traceback);
Py_DECREF(self->my_callback);
}
Py_TYPE(obj)->tp_free((PyObject*)self);
}
注意
在释放器函数中可以安全地做什么有限制。首先,如果你的类型支持垃圾容器(使用tp_traverse
和/或tp_clear
),一些对象的成员可以被清除或最终确定tp_dealloc
第二,在tp_dealloc
中,您的对象处于不稳定状态:它的引用计数等于零。任何调用非平凡对象或API(如上面的示例)可能最终再次调用tp_dealloc
,导致双重释放和崩溃。
从Python 3.4开始,建议不要在tp_dealloc
中放置任何复杂的最终化代码,而应使用新的tp_finalize
类型方法。
也可以看看
PEP 442说明新的最终化方案。
2.2.2. Object Presentation¶
在Python中,有两种方法来生成对象的文本表示:repr()
函数和str()
函数。(print()
函数只是调用str()
。)这些处理程序都是可选的。
reprfunc tp_repr;
reprfunc tp_str;
tp_repr
处理程序应返回一个字符串对象,该对象包含调用它的实例的表示。这里有一个简单的例子:
static PyObject *
newdatatype_repr(newdatatypeobject * obj)
{
return PyUnicode_FromFormat("Repr-ified_newdatatype{{size:\%d}}",
obj->obj_UnderlyingDatatypePtr->size);
}
如果未指定tp_repr
处理程序,则解释器将提供使用类型tp_name
的表示以及对象的唯一标识值。
tp_str
处理程序是str()
上面描述的tp_repr
处理程序是repr()
也就是说,当Python代码在对象的实例上调用str()
时,将调用它。它的实现非常类似于tp_repr
函数,但是生成的字符串是供人类使用的。如果未指定tp_str
,则使用tp_repr
处理程序。
这里有一个简单的例子:
static PyObject *
newdatatype_str(newdatatypeobject * obj)
{
return PyUnicode_FromFormat("Stringified_newdatatype{{size:\%d}}",
obj->obj_UnderlyingDatatypePtr->size);
}
2.2.3. Attribute Management¶
对于可以支持属性的每个对象,相应的类型必须提供控制属性如何解析的函数。需要有一个可以检索属性(如果定义了任何属性)的函数,另一个用于设置属性(如果允许设置属性)。删除属性是一种特殊情况,传递给处理程序的新值为NULL。
Python支持两对属性处理程序;支持属性的类型只需要实现一对的函数。区别在于一对将属性的名称作为char*
,而另一对接受PyObject*
。每种类型可以使用任何对更有意义的实施的方便。
getattrfunc tp_getattr; /* char * version */
setattrfunc tp_setattr;
/* ... */
getattrofunc tp_getattro; /* PyObject * version */
setattrofunc tp_setattro;
如果访问对象的属性总是一个简单的操作(这将很快解释),有一些通用的实现可以用于提供属性管理函数的PyObject*
版本。从Python 2.2开始,类型特定属性处理程序的实际需求几乎完全消失了,尽管有许多例子没有被更新以使用一些可用的新的通用机制。
2.2.3.1. Generic Attribute Management¶
大多数扩展类型只使用简单属性。那么,什么使得属性简单?只有几个条件必须满足:
- 当调用
PyType_Ready()
时,必须知道属性的名称。 - 不需要进行特殊处理来记录查找或设置属性,也不需要基于该值执行操作。
请注意,此列表不会对属性的值,计算的值或相关数据的存储方式施加任何限制。
当调用PyType_Ready()
时,它使用由类型对象引用的三个表来创建放置在类型对象的字典中的descriptor。每个描述器控制对实例对象的一个属性的访问。每个表是可选的;如果所有三个都是NULL,则类型的实例将仅具有从其基类型继承的属性,并应保留tp_getattro
和tp_setattro
字段NULL,允许基本类型处理属性。
表被声明为类型对象的三个字段:
struct PyMethodDef *tp_methods;
struct PyMemberDef *tp_members;
struct PyGetSetDef *tp_getset;
如果tp_methods
不是NULL,则必须引用PyMethodDef
结构的数组。表中的每个条目都是此结构的实例:
typedef struct PyMethodDef {
char *ml_name; /* method name */
PyCFunction ml_meth; /* implementation function */
int ml_flags; /* flags */
char *ml_doc; /* docstring */
} PyMethodDef;
应为类型提供的每个方法定义一个条目;从基本类型继承的方法不需要任何条目。最后需要一个额外的条目;它是一个标记数组结束的标记。哨兵的ml_name
字段必须为NULL。
第二个表用于定义直接映射到实例中存储的数据的属性。支持各种基本C类型,并且访问可以是只读的或读写的。表中的结构定义为:
typedef struct PyMemberDef {
char *name;
int type;
int offset;
int flags;
char *doc;
} PyMemberDef;
对于表中的每个条目,将构造descriptor并添加到将能够从实例结构中提取值的类型。type
字段应包含structmember.h
头中定义的类型代码之一;该值将用于确定如何将Python值与C值进行转换。flags
字段用于存储控制如何访问属性的标志。
以下标志常量在structmember.h
中定义;它们可以使用按位或运算进行组合。
不变 | 含义 |
---|---|
READONLY | 不可写。 |
READ_RESTRICTED | 在限制模式下不可读。 |
WRITE_RESTRICTED | 在限制模式下不可写。 |
RESTRICTED | 在受限模式下不可读或可写。 |
使用tp_members
表来构建在运行时使用的描述器的一个有趣的优点是,以这种方式定义的任何属性可以通过提供表中的文本来具有相关联的文档字符串。应用程序可以使用内省API从类对象中检索描述器,并使用其__doc__
属性获取文档字符串。
与tp_methods
表一样,需要具有name
值为NULL的哨兵条目。
2.2.3.2. Type-specific Attribute Management¶
为简单起见,这里只演示char*
版本; name参数的类型是接口的char*
和PyObject*
类型之间的唯一区别。此示例有效地执行与上述通用示例相同的操作,但不使用在Python 2.2中添加的通用支持。它解释了如何调用处理函数,所以如果你需要扩展它们的功能,你会明白需要做什么。
当对象需要属性查找时,调用tp_getattr
处理程序。它在类的__getattr__()
方法将被调用的相同情况下调用。
这里是一个例子:
static PyObject *
newdatatype_getattr(newdatatypeobject *obj, char *name)
{
if (strcmp(name, "data") == 0)
{
return PyLong_FromLong(obj->data);
}
PyErr_Format(PyExc_AttributeError,
"'%.50s' object has no attribute '%.400s'",
tp->tp_name, name);
return NULL;
}
当将调用类实例的__setattr__()
或__delattr__()
方法时,调用tp_setattr
处理程序。当一个属性应该被删除时,第三个参数将是NULL。这里是一个简单引用异常的例子;如果这真的是你想要的,tp_setattr
处理程序应该设置为NULL。
static int
newdatatype_setattr(newdatatypeobject *obj, char *name, PyObject *v)
{
(void)PyErr_Format(PyExc_RuntimeError, "Read-only attribute: \%s", name);
return -1;
}
2.2.4. Object Comparison¶
richcmpfunc tp_richcompare;
当需要比较时,调用tp_richcompare
处理程序。它类似于rich comparison methods,如__lt__()
,也由PyObject_RichCompare()
和PyObject_RichCompareBool()
This function is called with two Python objects and the operator as arguments, where the operator is one of Py_EQ
, Py_NE
, Py_LE
, Py_GT
, Py_LT
or Py_GT
. 它应该比较两个对象与指定的操作符号,如果比较成功,返回Py_True
或Py_False
,Py_NotImplemented
未实现,应尝试其他对象的比较方法,如果设置了异常,则NULL。
下面是一个示例实现,对于一个数据类型,如果内部指针的大小相等,则认为它是相等的:
static PyObject *
newdatatype_richcmp(PyObject *obj1, PyObject *obj2, int op)
{
PyObject *result;
int c, size1, size2;
/* code to make sure that both arguments are of type
newdatatype omitted */
size1 = obj1->obj_UnderlyingDatatypePtr->size;
size2 = obj2->obj_UnderlyingDatatypePtr->size;
switch (op) {
case Py_LT: c = size1 < size2; break;
case Py_LE: c = size1 <= size2; break;
case Py_EQ: c = size1 == size2; break;
case Py_NE: c = size1 != size2; break;
case Py_GT: c = size1 > size2; break;
case Py_GE: c = size1 >= size2; break;
}
result = c ? Py_True : Py_False;
Py_INCREF(result);
return result;
}
2.2.5. Abstract Protocol Support¶
Python支持各种抽象'协议;提供使用这些接口的具体接口在Abstract Objects Layer中记录。
许多这些抽象接口是在Python实现的开发早期定义的。特别地,数字,映射和序列协议自Python开始就是Python的一部分。随着时间的推移,增加了其他协议。对于依赖于来自类型实现的几个处理程序例程的协议,较旧的协议已经被定义为由类型对象引用的处理程序的可选块。对于较新的协议,在主类型对象中有附加的槽,其中标志位被设置为指示槽存在并且应当由解释器检查。(标志位不指示时隙值非NULL。该标志可以被设置为指示时隙的存在,但是时隙仍然可能未被填充。
PyNumberMethods *tp_as_number;
PySequenceMethods *tp_as_sequence;
PyMappingMethods *tp_as_mapping;
If you wish your object to be able to act like a number, a sequence, or a mapping object, then you place the address of a structure that implements the C type PyNumberMethods
, PySequenceMethods
, or PyMappingMethods
, respectively. 它是由你来填充这个结构适当的值。您可以在Python源代码分发的Objects
目录中找到使用这些代码的示例。
hashfunc tp_hash;
这个函数,如果你选择提供它,应该返回一个哈希数的实例的数据类型。这里是一个适度无意义的例子:
static long
newdatatype_hash(newdatatypeobject *obj)
{
long result;
result = obj->obj_UnderlyingDatatypePtr->size;
result = result * 3;
return result;
}
ternaryfunc tp_call;
This function is called when an instance of your data type is “called”, for example, if obj1
is an instance of your data type and the Python script contains obj1('hello')
, the tp_call
handler is invoked.
此函数有三个参数:
- arg1是作为调用主体的数据类型的实例。如果调用
obj1('hello')
,则arg1是obj1
。 - arg2是一个包含调用参数的元组。您可以使用
PyArg_ParseTuple()
提取参数。 - arg3是传递的关键字参数的字典。如果这是非NULL并且您支持关键字参数,请使用
PyArg_ParseTupleAndKeywords()
提取参数。如果您不想支持关键字参数,并且这不是NULL,请引入一个TypeError
,并显示不支持关键字参数的消息。
这里是调用函数的实现的一个荒谬的例子。
/* Implement the call function.
* obj1 is the instance receiving the call.
* obj2 is a tuple containing the arguments to the call, in this
* case 3 strings.
*/
static PyObject *
newdatatype_call(newdatatypeobject *obj, PyObject *args, PyObject *other)
{
PyObject *result;
char *arg1;
char *arg2;
char *arg3;
if (!PyArg_ParseTuple(args, "sss:call", &arg1, &arg2, &arg3)) {
return NULL;
}
result = PyUnicode_FromFormat(
"Returning -- value: [\%d] arg1: [\%s] arg2: [\%s] arg3: [\%s]\n",
obj->obj_UnderlyingDatatypePtr->size,
arg1, arg2, arg3);
return result;
}
/* Iterators */
getiterfunc tp_iter;
iternextfunc tp_iternext;
这些函数为迭代器协议提供支持。任何希望支持对其内容(可以在迭代期间生成)的迭代的对象必须实现tp_iter
处理程序。由tp_iter
处理程序返回的对象必须同时实现tp_iter
和tp_iternext
处理程序。两个处理程序都只需要一个参数,它们被调用的实例,并返回一个新的引用。在出现错误的情况下,它们应设置异常并返回NULL。
对于表示可迭代容器的对象,tp_iter
处理程序必须返回一个迭代器对象。迭代器对象负责维护迭代的状态。对于可以支持多个不相互干扰的迭代器(如列表和元组所做)的容器,应该创建并返回一个新的迭代器。只能迭代一次(通常是由于迭代的副作用)的对象应该通过返回一个新的引用来实现这个处理程序,并且还应该实现tp_iternext
处理程序。文件对象是这种迭代器的一个例子。
迭代器对象应该实现两个处理程序。tp_iter
处理程序应该返回对迭代器的新引用(这与对只能通过破坏性迭代的对象的tp_iter
处理程序相同)。如果有一个对象,tp_iternext
处理程序应该返回对迭代中的下一个对象的新引用。如果迭代已经到达结束,它可以返回NULL而不设置异常,或者可以设置StopIteration
;避免异常可以产生略微更好的性能。如果发生实际错误,则应设置异常并返回NULL。
2.2.6. Weak Reference Support¶
Python的弱引用实现的目标之一是允许任何类型参与弱引用机制,而不会导致那些没有受益于弱引用(例如数字)的对象的开销。
对于弱引用的对象,扩展必须在实例结构中包含一个PyObject*
字段,以便使用弱引用机制;它必须通过对象的构造函数初始化为NULL。它还必须将相应类型对象的tp_weaklistoffset
字段设置为字段的偏移量。例如,实例类型定义有以下结构:
typedef struct {
PyObject_HEAD
PyClassObject *in_class; /* The class object */
PyObject *in_dict; /* A dictionary */
PyObject *in_weakreflist; /* List of weak references */
} PyInstanceObject;
实例的静态声明的类型对象以这种方式定义:
PyTypeObject PyInstance_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
0,
"module.instance",
/* Lots of stuff omitted for brevity... */
Py_TPFLAGS_DEFAULT, /* tp_flags */
0, /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
offsetof(PyInstanceObject, in_weakreflist), /* tp_weaklistoffset */
};
类型构造函数负责将弱引用列表初始化为NULL:
static PyObject *
instance_new() {
/* Other initialization stuff omitted for brevity */
self->in_weakreflist = NULL;
return (PyObject *) self;
}
唯一进一步的添加是析构函数需要调用弱引用管理器来清除任何弱引用。仅当弱引用列表为非NULL时,才需要这样做:
static void
instance_dealloc(PyInstanceObject *inst)
{
/* Allocate temporaries if needed, but do not begin
destruction just yet.
*/
if (inst->in_weakreflist != NULL)
PyObject_ClearWeakRefs((PyObject *) inst);
/* Proceed with object destruction normally. */
}
2.2.7. More Suggestions¶
请记住,您可以省略大多数这些函数,在这种情况下,您提供0
作为值。对于必须提供的每个函数,都有类型定义。它们位于Python源代码发行版附带的Python include目录中的object.h
中。
为了学习如何为您的新数据类型实现任何特定的方法,请执行以下操作:下载和分拆Python源代码分发。转到Objects
目录,然后在C源文件中搜索tp_
加上所需的函数(例如,tp_richcompare
)。您将找到要实现的函数的示例。
当您需要验证对象是您正在实现的类型的实例时,使用PyObject_TypeCheck()
函数。其使用的示例可能如下所示:
if (! PyObject_TypeCheck(some_object, &MyType)) {
PyErr_SetString(PyExc_TypeError, "arg #1 not a mything");
return NULL;
}
脚注
[1] | 这是真的,当我们知道对象是一个基本类型,如字符串或浮点。 |
[2] | 在本示例中,我们在tp_dealloc 处理程序中依赖于此,因为我们的类型不支持垃圾容器。即使一个类型支持垃圾容器,也有可以从垃圾容器“untrack”对象的调用,但是,这些调用是高级的,这里不包括。 |
[3] | 我们现在知道第一个和最后一个成员是字符串,所以也许我们可以不那么小心减少它们的引用计数,但是,我们接受字符串子类的实例。即使释放正常的字符串不会回调到我们的对象,我们不能保证解除一个字符串子类的实例不会回调到我们的对象。 |
[4] | 即使在第三版本中,我们也不能保证避免循环。允许字符串子类的实例,并且字符串子类可以允许循环,即使正常字符串不允许。 |