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;
    Py_ssize_t tp_vectorcall_offset;
    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;

这是一个 lot 方法。不过,不要担心太多——如果您有一个想要定义的类型,那么很可能只实现其中的一小部分。

正如您现在可能期望的那样,我们将讨论这个问题,并提供有关各种处理程序的更多信息。我们不会按照结构中定义的顺序执行,因为有许多历史包袱会影响字段的排序。通常最容易找到一个包含所需字段的示例,然后更改值以适应新类型。::

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);
}

DealLocator函数的一个重要要求是,它只保留任何挂起的异常。这一点很重要,因为解释器在解释器释放python堆栈时经常调用dealLocator;当由于异常(而不是正常返回)而解除堆栈时,不会采取任何措施来保护dealLocator,使其看不到已经设置了异常。DealLocator执行的任何可能导致执行其他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_CallNoArgs(self->my_callback);
        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);
}

注解

在DealLocator函数中,您可以安全地执行的操作有一些限制。首先,如果您的类型支持垃圾收集(使用 tp_traverse 和/或 tp_clear )时,对象的某些成员可能已被清除或完成。 tp_dealloc 被称为。第二,在 tp_dealloc ,您的对象处于不稳定状态:其引用计数等于零。对一个重要对象或API的任何调用(如上面的示例中所示)都可能最终导致调用 tp_dealloc 再次,造成了双重自由和碰撞。

从Python3.4开始,建议不要在 tp_dealloc 而是使用新的 tp_finalize 类型方法。

参见

PEP 442 解释新的定案方案。

3.2. 对象表示

在Python中,有两种方法可以生成对象的文本表示: repr() 函数,以及 str() 功能。(The 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. 通用属性管理

大多数扩展类型只使用 简单的 属性。那么,是什么让属性变得简单呢?只有几个条件必须满足:

  1. PyType_Ready() 被称为。

  2. 不需要特殊处理来记录已查找或设置的属性,也不需要基于该值采取操作。

请注意,此列表不会对属性值、计算值的时间或存储相关数据的方式进行任何限制。

什么时候? PyType_Ready() 调用时,它使用类型对象引用的三个表来创建 descriptor 位于类型对象的字典中的。每个描述符控制对实例对象的一个属性的访问。每个表都是可选的;如果这三个表都是 NULL ,类型的实例将只具有从其基类型继承的属性,并且应保留 tp_getattrotp_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;

对于表中的每个条目, descriptor 将被构造并添加到能够从实例结构中提取值的类型。这个 type 字段应包含在 structmember.h header;该值将用于确定如何将python值与c值进行转换。这个 flags 字段用于存储控制如何访问属性的标志。

在中定义了以下标志常量 structmember.h ;它们可以使用按位或组合。

常数

意义

READONLY

决不可写。

READ_RESTRICTED

在受限模式下不可读。

WRITE_RESTRICTED

在受限模式下不可写。

RESTRICTED

在受限模式下不可读或不可写。

使用 tp_members 构建在运行时使用的描述符的表是,通过提供表中的文本,任何以这种方式定义的属性都可以有一个关联的文档字符串。应用程序可以使用自省API从类对象中检索描述符,并使用其 __doc__ 属性。

如同 tp_methods 表,一个带有 name 价值 NULL 是必需的。

3.3.2. 类型特定属性管理

为了简单起见,只有 char* 这里将演示版本;名称参数的类型是 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 当需要比较时调用处理程序。它类似于 rich comparison methods ,像 __lt__() ,也由调用 PyObject_RichCompare()PyObject_RichCompareBool() .

使用两个python对象调用此函数,并将运算符作为参数,其中运算符是 Py_EQPy_NEPy_LEPy_GTPy_LTPy_GT . 它应该将这两个对象与指定的运算符进行比较并返回 Py_TruePy_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支持多种 摘要 “Protocols;”为使用这些接口而提供的特定接口记录在 抽象对象层 .

在Python实现的早期开发中定义了许多这些抽象接口。特别是,从一开始,数字、映射和序列协议就一直是Python的一部分。随着时间的推移,已经添加了其他协议。对于依赖于类型实现中的几个处理程序例程的协议,旧协议被定义为类型对象引用的处理程序的可选块。对于较新的协议,主类型对象中有额外的插槽,设置一个标志位来指示插槽存在,并且应该由解释器检查。(标志位不表示插槽值为非-NULL。标记可以设置为指示插槽的存在,但插槽可能仍然未填充。):

PyNumberMethods   *tp_as_number;
PySequenceMethods *tp_as_sequence;
PyMappingMethods  *tp_as_mapping;

如果希望对象能够像数字、序列或映射对象一样工作,则可以放置实现C类型的结构的地址。 PyNumberMethodsPySequenceMethodsPyMappingMethods ,分别。由您来用适当的值填充这个结构。您可以在 Objects python源分发的目录。::

hashfunc tp_hash;

如果选择提供此函数,则此函数应返回数据类型实例的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 是带符号的整数类型,平台宽度可变。返回 -1tp_hash 指示一个错误,这就是为什么在hash计算成功时应小心避免返回该错误的原因,如上所示。

ternaryfunc tp_call;

当数据类型的实例被“调用”时,例如,如果 obj1 是数据类型的实例,python脚本包含 obj1('hello') , the tp_call 调用了处理程序。

此函数接受三个参数:

  1. self 是作为调用主题的数据类型的实例。如果调用是 obj1('hello') 然后 selfobj1 .

  2. args 是一个包含调用参数的元组。你可以使用 PyArg_ParseTuple() 提取参数。

  3. 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 处理程序。

Any iterator object should implement both tp_iter and tp_iternext. An iterator's tp_iter handler should return a new reference to the iterator. Its tp_iternext handler should return a new reference to the next object in the iteration, if there is one. If the iteration has reached the end, tp_iternext may return NULL without setting an exception, or it may set StopIteration in addition to returning NULL; avoiding the exception can yield slightly better performance. If an actual error occurs, tp_iternext should always set an exception and return NULL.

3.6. 弱参考支撑

Python弱引用实现的目标之一是允许任何类型参与弱引用机制,而不会导致性能关键对象(如数字)的开销。

参见

文件 weakref 模块。

对于弱可引用的对象,扩展类型必须执行两项操作:

  1. 包括一个 PyObject* 场在C对象结构中专门用于弱参考机制。对象的构造函数应该离开它 NULL (使用默认值时自动 tp_alloc

  2. 设置 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),
};

唯一的补充是 tp_dealloc 需要清除任何弱引用(通过调用 PyObject_ClearWeakRefs() )如果字段为非-NULL

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