3. 定义扩展类型:分类主题 — Python 文档
3. 定义扩展类型:分类主题
本节旨在快速浏览您可以实现的各种类型方法以及它们的作用。
这是 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;
现在这是 批 的方法。 不过不要太担心——如果你有一个你想定义的类型,你很可能只实现其中的一小部分。
正如您现在可能期望的那样,我们将对此进行讨论并提供有关各种处理程序的更多信息。 我们不会按照它们在结构中定义的顺序进行介绍,因为有很多影响字段顺序的历史包袱。 通常最容易找到一个包含您需要的字段的示例,然后更改值以适合您的新类型。
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__
以检索文档字符串时,您希望返回该字符串(或其地址)。
现在我们来看看基本类型方法——大多数扩展类型将实现的方法。
3.1. 完成和取消分配
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 中,您的对象处于不稳定状态:其引用计数等于 0。 任何对非平凡对象或 API 的调用(如上例所示)可能最终会再次调用 tp_dealloc,从而导致双重释放和崩溃。
从 Python 3.4 开始,建议不要在 tp_dealloc 中放置任何复杂的终结代码,而是使用新的 tp_finalize 类型方法。
3.2. 对象展示
在 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);
}
3.3. 属性管理
对于每个可以支持属性的对象,相应的类型必须提供控制如何解析属性的函数。 需要有一个函数可以检索属性(如果定义了),另一个函数可以设置属性(如果允许设置属性)。 删除属性是一种特殊情况,传递给处理程序的新值是 NULL
。
Python 支持两对属性处理程序; 支持属性的类型只需要实现一对函数。 不同之处在于一对将属性名称作为 char*
,而另一对接受 PyObject*
。 为了实现的方便,每种类型都可以使用更有意义的一对。
getattrfunc tp_getattr; /* char * version */
setattrfunc tp_setattr;
/* ... */
getattrofunc tp_getattro; /* PyObject * version */
setattrofunc tp_setattro;
如果访问对象的属性始终是一个简单的操作(这将在稍后解释),则可以使用通用实现来提供 PyObject*
版本的属性管理功能。 从 Python 2.2 开始,对特定于类型的属性处理程序的实际需求几乎完全消失了,尽管有许多示例尚未更新以使用一些可用的新通用机制。
3.3.1. 通用属性管理
大多数扩展类型只使用 simple 属性。 那么,是什么让属性变得简单呢? 只有两个条件必须满足:
- 调用 PyType_Ready() 时必须知道属性的名称。
- 不需要特殊处理来记录属性被查找或设置,也不需要基于值采取行动。
请注意,此列表对属性值、计算值的时间或相关数据的存储方式没有任何限制。
当 PyType_Ready() 被调用时,它使用类型对象引用的三个表来创建 描述符 ,这些表被放置在类型对象的字典中。 每个描述符控制对实例对象的一个属性的访问。 每个表都是可选的; 如果所有三个都是 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 {
const char *ml_name; /* method name */
PyCFunction ml_meth; /* implementation function */
int ml_flags; /* flags */
const char *ml_doc; /* docstring */
} PyMethodDef;
应该为类型提供的每个方法定义一个条目; 从基类型继承的方法不需要条目。 最后需要一个额外的条目; 它是一个标记数组结束的哨兵。 哨兵的ml_name
字段必须是NULL
。
第二个表用于定义直接映射到存储在实例中的数据的属性。 支持多种原始 C 类型,访问可以是只读或读写。 表中的结构定义为:
typedef struct PyMemberDef {
const char *name;
int type;
int offset;
int flags;
const char *doc;
} PyMemberDef;
对于表中的每个条目,将构造一个 描述符 并将其添加到能够从实例结构中提取值的类型中。 type 字段应包含 structmember.h
标头中定义的类型代码之一; 该值将用于确定如何将 Python 值与 C 值相互转换。 flags
字段用于存储控制如何访问属性的标志。
structmember.h
中定义了以下标志常量; 它们可以使用按位或进行组合。
持续的 | 意义 |
---|---|
READONLY
|
永远不可写。 |
READ_RESTRICTED
|
在受限模式下不可读。 |
WRITE_RESTRICTED
|
在受限模式下不可写。 |
RESTRICTED
|
在受限模式下不可读或不可写。 |
使用 tp_members 表构建在运行时使用的描述符的一个有趣优点是,通过在表中提供文本,任何以这种方式定义的属性都可以具有关联的文档字符串。 应用程序可以使用内省 API 从类对象中检索描述符,并使用其 __doc__
属性获取文档字符串。
与 tp_methods 表一样,需要 name
值为 NULL
的标记条目。
3.3.2. 特定于类型的属性管理
为简单起见,这里只展示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;
}
tp_setattr 处理程序在调用类实例的 __setattr__()
或 __delattr__()
方法时被调用。 当一个属性应该被删除时,第三个参数将是 NULL
。 这是一个简单地引发异常的示例; 如果这真的是您想要的,那么 tp_setattr 处理程序应该设置为 NULL
。
static int
newdatatype_setattr(newdatatypeobject *obj, char *name, PyObject *v)
{
PyErr_Format(PyExc_RuntimeError, "Read-only attribute: %s", name);
return -1;
}
3.4. 对象比较
richcmpfunc tp_richcompare;
tp_richcompare 处理程序在需要比较时被调用。 它类似于丰富的比较方法,如__lt__()
,也被PyObject_RichCompare()和PyObject_RichCompareBool()调用。
此函数使用两个 Python 对象和运算符作为参数调用,其中运算符是 Py_EQ
、Py_NE
、Py_LE
、Py_GT
、Py_LT
或 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;
}
3.5. 抽象协议支持
Python 支持多种 abstract 'protocols;' 为使用这些接口而提供的特定接口记录在 抽象对象层 中。
许多这些抽象接口是在 Python 实现的早期定义的。 特别是,数字、映射和序列协议从一开始就是 Python 的一部分。 随着时间的推移,已经添加了其他协议。 对于依赖于类型实现的多个处理程序例程的协议,旧协议已被定义为类型对象引用的可选处理程序块。 对于较新的协议,主类型对象中有额外的插槽,设置一个标志位以指示插槽存在并且应该由解释器检查。 (标志位并不表示插槽值是非 NULL
。 可以设置标志以指示插槽的存在,但插槽可能仍然未填充。)
PyNumberMethods *tp_as_number;
PySequenceMethods *tp_as_sequence;
PyMappingMethods *tp_as_mapping;
如果您希望您的对象能够像数字、序列或映射对象一样工作,那么您可以放置实现 C 类型 PyNumberMethods、PySequenceMethods 的结构的地址, 或 PyMappingMethods,分别。 您可以用适当的值填充此结构。 您可以在 Python 源代码分发的 Objects
目录中找到使用这些的示例。
hashfunc tp_hash;
这个函数,如果你选择提供它,应该为你的数据类型的一个实例返回一个哈希数。 这是一个简单的例子:
static Py_hash_t
newdatatype_hash(newdatatypeobject *obj)
{
Py_hash_t result;
result = obj->some_size + 32767 * obj->some_number;
if (result == -1)
result = -2;
return result;
}
Py_hash_t
是一个有符号整数类型,宽度随平台变化。 从 tp_hash 返回 -1
表示错误,这就是为什么你应该小心避免在哈希计算成功时返回它,如上所示。
ternaryfunc tp_call;
当您的数据类型的实例被“调用”时调用此函数,例如,如果 obj1
是您的数据类型的实例并且 Python 脚本包含 obj1('hello')
,则 tp_call 处理程序被调用。
这个函数接受三个参数:
- self 是作为调用对象的数据类型的实例。 如果调用的是
obj1('hello')
,那么self就是obj1
。 - args 是一个包含调用参数的元组。 您可以使用 PyArg_ParseTuple() 来提取参数。
- kwds 是传递的关键字参数的字典。 如果这是非
NULL
并且您支持关键字参数,请使用 PyArg_ParseTupleAndKeywords() 提取参数。 如果您不想支持关键字参数并且这是非NULL
,则引发 TypeError 并显示不支持关键字参数的消息。
这是一个玩具 tp_call
实现:
static PyObject *
newdatatype_call(newdatatypeobject *self, PyObject *args, PyObject *kwds)
{
PyObject *result;
const char *arg1;
const char *arg2;
const 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;
这些函数为迭代器协议提供支持。 两个处理程序都只接受一个参数,即调用它们的实例,并返回一个新的引用。 在出现错误的情况下,他们应该设置一个异常并返回 NULL
。 tp_iter对应Python的__iter__()
方法,而tp_iternext对应Python的__next__()方法。
任何 iterable 对象都必须实现 tp_iter 处理程序,该处理程序必须返回一个 iterator 对象。 此处适用与 Python 类相同的准则:
- 对于可以支持多个独立迭代器的集合(例如列表和元组),每次调用 tp_iter 时都应该创建并返回一个新的迭代器。
- 只能迭代一次的对象(通常是由于迭代的副作用,例如文件对象)可以通过返回对自身的新引用来实现 tp_iter——因此也应该实现 tp_iternext[ X241X] 处理程序。
任何 iterator 对象都应该实现 tp_iter 和 tp_iternext。 迭代器的 tp_iter 处理程序应该返回对迭代器的新引用。 它的 tp_iternext 处理程序应该返回对迭代中下一个对象的新引用,如果有的话。 如果迭代已经结束,tp_iternext可能会返回NULL
而不设置异常,或者设置StopIteration另外返回[ X181X]; 避免异常可以产生稍微更好的性能。 如果发生实际错误,tp_iternext 应始终设置异常并返回 NULL
。
3.6. 弱引用支持
Python 弱引用实现的目标之一是允许任何类型参与弱引用机制,而不会产生性能关键对象(例如数字)的开销。
对于弱引用的对象,扩展类型必须做两件事:
- 在专用于弱引用机制的 C 对象结构中包含
PyObject*
字段。 对象的构造函数应该保留它NULL
(当使用默认的 tp_alloc 时这是自动的)。 - 将 tp_weaklistoffset 类型成员设置为 C 对象结构中上述字段的偏移量,以便解释器知道如何访问和修改该字段。
具体来说,以下是如何使用所需字段来扩充一个简单的对象结构:
typedef struct {
PyObject_HEAD
PyObject *weakreflist; /* List of weak references */
} TrivialObject;
以及静态声明类型对象中的相应成员:
static PyTypeObject TrivialType = {
PyVarObject_HEAD_INIT(NULL, 0)
/* ... other members omitted for brevity ... */
.tp_weaklistoffset = offsetof(TrivialObject, weakreflist),
};
唯一的进一步补充是,如果字段是非 NULL
,则 tp_dealloc
需要清除任何弱引用(通过调用 PyObject_ClearWeakRefs()
):
static void
Trivial_dealloc(TrivialObject *self)
{
/* Clear weakrefs first before calling any destructors */
if (self->weakreflist != NULL)
PyObject_ClearWeakRefs((PyObject *) self);
/* ... remainder of destruction code omitted for brevity ... */
Py_TYPE(self)->tp_free((PyObject *) self);
}
3.7. 更多建议
要了解如何为新数据类型实现任何特定方法,请获取 CPython 源代码。 进入 Objects
目录,然后在 C 源文件中搜索 tp_
加上你想要的函数(例如,tp_richcompare
)。 您将找到要实现的功能的示例。
当您需要验证对象是否是您正在实现的类型的具体实例时,请使用 PyObject_TypeCheck() 函数。 它的使用示例可能如下所示:
if (!PyObject_TypeCheck(some_object, &MyType)) {
PyErr_SetString(PyExc_TypeError, "arg #1 not a mything");
return NULL;
}
也可以看看
- 下载 CPython 源代码版本。
- https://www.python.org/downloads/source/
- GitHub 上的 CPython 项目,其中开发了 CPython 源代码。
- https://github.com/python/cpython