python / 3.7.2rc1 / all / extending-newtypes.html

3. 定义扩展类型:各种主题

本节旨在快速介绍您可以实现的各种类型的方法及其作用。

这是PyTypeObject的定义,其中一些仅在调试版本中使用的字段被Ellipsis:

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;

现在,这是很多方法。不过,不必太担心-如果您要定义一个类型,则很有可能只实现其中的少数几个。

正如您现在可能期望的那样,我们将介绍这一点,并提供有关各种处理程序的更多信息。我们不会按照结构中定义的 Sequences 进行排序,因为有很多历史包 bag 会影响字段的排序。通常最容易找到包含所需字段的示例,然后更改值以适合您的新类型。

const char *tp_name; /* For printing */

类型的名称–如上一章所述,它将出现在不同的地方,几乎完全出于诊断目的。try选择在这种情况下会有所帮助的东西!

Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

这些字段告诉运行时创建此类型的新对象时要分配多少内存。 Python 内置了对可变长度结构(例如:字符串,Tuples)的支持,这是tp_itemsize字段的来源。稍后将对此进行处理。

const char *tp_doc;

在这里,您可以放置一个当 Python 脚本引用obj.__doc__时要返回的字符串(或其地址)以检索 doc 字符串。

现在我们来介绍基本的类型方法-大多数扩展类型将实现的方法。

3.1. 完成和取消分配

destructor tp_dealloc;

当您的类型实例的引用计数减少到零并且 Python 解释器想要回收该函数时,将调用此函数。如果您的类型有可用的内存或要执行的其他清理操作,则可以将其放在此处。对象本身也需要在这里释放。这是此Function的示例:

static void
newdatatype_dealloc(newdatatypeobject *obj)
{
    free(obj->obj_UnderlyingDatatypePtr);
    Py_TYPE(obj)->tp_free(obj);
}

解除分配器Function的一项重要要求是,它应独自保留所有未决异常。这很重要,因为在解释器展开 Python 堆栈时,经常会调用 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_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);
}

Note

在解除分配器函数中可以安全执行的操作存在一些限制。首先,如果您的类型支持垃圾回收(使用tp_traverse和/或tp_clear),则在调用tp_dealloc时可以清除或确定对象的某些成员。其次,在tp_dealloc中,您的对象处于不稳定状态:其引用计数等于零。对非平凡对象或 API 的任何调用(如上例所示)可能finally再次调用tp_dealloc,从而导致双重释放和崩溃。

从 Python 3.4 开始,建议不要在tp_dealloc中放入任何复杂的完成代码,而应使用新的tp_finalize类型的方法。

See also

PEP 442解释了新的finally确定方案。

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. 属性 Management

对于每个可以支持属性的对象,相应的类型必须提供控制如何解析属性的Function。需要有一个函数可以检索属性(如果已定义),而另一个函数可以设置属性(如果允许设置属性)。删除属性是一种特殊情况,传递给处理程序的新值是NULL

Python 支持两对属性处理程序。支持属性的类型只需要实现PairFunction即可。区别在于,Pair将属性名称命名为char*,而另Pair则接受PyObject*。每种类型都可以使用对实现更方便的任何对。

getattrfunc  tp_getattr;        /* char * version */
setattrfunc  tp_setattr;
/* ... */
getattrofunc tp_getattro;       /* PyObject * version */
setattrofunc tp_setattro;

如果访问对象的属性始终是一个简单的操作(稍后将对此进行说明),则可以使用一些通用实现来提供属性 Management Function的PyObject*版本。从 Python 2.2 开始,对特定于类型的属性处理程序的实际需求几乎完全消失了,尽管有许多示例尚未更新以使用某些可用的新通用机制。

3.3.1. 通用属性 Management

大多数扩展类型仅使用* simple *属性。那么,什么使属性简单?仅需满足几个条件:

  • 调用PyType_Ready()时必须知道属性的名称。

  • 无需进行特殊处理即可记录已查找或设置的属性,也无需根据该值采取任何操作。

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

调用PyType_Ready()时,它将使用类型对象引用的三个表来创建descriptor,并将它们放置在类型对象的字典中。每个 Descriptors 控制对实例对象的一个属性的访问。每个表都是可选的;如果所有三个均为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

第二个表用于定义直接 Map 到实例中存储的数据的属性。支持多种原始 C 类型,访问可以是只读或读写的。表中的结构定义为:

typedef struct PyMemberDef {
    const char *name;
    int         type;
    int         offset;
    int         flags;
    const char *doc;
} PyMemberDef;

对于表中的每个条目,将构造一个descriptor并将其添加到类型中,该类型将能够从实例结构中提取值。 type字段应包含structmember.hHeaders 中定义的类型代码之一;该值将用于确定如何将 Python 值与 C 值相互转换。 flags字段用于存储标志,这些标志控制如何访问属性。

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

Constant Meaning
READONLY Never writable.
READ_RESTRICTED 在受限模式下不可读。
WRITE_RESTRICTED 在受限模式下不可写。
RESTRICTED 在受限模式下不可读或不可写。

使用tp_members表构建在运行时使用的 Descriptors 的一个有趣的优点是,只需pass在表中提供文本,以此方式定义的任何属性都可以具有关联的文档字符串。应用程序可以使用自省 API 从类对象检索 Descriptors,并使用其__doc__属性获取 doc 字符串。

tp_methods表一样,需要name值为NULL的标记条目。

3.3.2. 特定类型的属性 Management

为简单起见,此处仅展示char*版本;名称参数的类型是界面的char*PyObject*风格之间的唯一区别。该示例实际上执行与上述通用示例相同的操作,但是未使用 Python 2.2 中添加的通用支持。它说明了如何调用处理程序函数,这样,如果您确实需要扩展其Function,就可以了解需要做什么。

当对象需要属性查询时,将调用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)
{
    PyErr_Format(PyExc_RuntimeError, "Read-only attribute: %s", name);
    return -1;
}

3.4. 对象比较

richcmpfunc tp_richcompare;

需要比较时,将调用tp_richcompare处理程序。与_3 类似,类似于lt(),也由PyObject_RichCompare()PyObject_RichCompareBool()调用。

该函数使用两个 Python 对象和运算符作为参数调用,其中运算符是Py_EQPy_NEPy_LEPy_GTPy_LTPy_GT之一。它应针对指定的运算符比较这两个对象,如果比较成功,则返回Py_TruePy_False;返回Py_NotImplemented表示未实现比较,应try另一个对象的比较方法;如果设置了异常,则返回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 支持多种抽象协议; 抽象对象层中记录了提供使用这些接口的特定接口。

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

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

如果希望对象能够像数字,序列或 Map 对象一样工作,则可以放置分别实现 C 类型PyNumberMethodsPySequenceMethodsPyMappingMethods的结构的地址。您可以自行填写适当的值。您可以在 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 *是一个包含调用参数的 Tuples。您可以使用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;

这些Function提供对迭代器协议的支持。这两个处理程序仅使用一个参数(为其调用实例),并返回一个新引用。如果发生错误,他们应该设置一个异常并返回NULLtp_iter对应于 Python iter()方法,而tp_iternext对应于 Python next()方法。

任何iterable对象都必须实现tp_iter处理程序,该处理程序必须返回iterator对象。在这里,相同的准则适用于 Python 类:

  • 对于可以支持多个独立迭代器的集合(例如列表和 Tuples),应创建一个新的迭代器,并在每次调用tp_iter时将其返回。

  • 只能迭代一次的对象(通常是由于迭代的副作用,例如文件对象)可以pass返回对自身的新引用来实现tp_iter,因此也应该实现tp_iternext处理函数。

任何iterator对象都应同时实现tp_itertp_iternext。迭代器的tp_iter处理程序应返回对该迭代器的新引用。如果存在,它的tp_iternext处理程序应返回对迭代中下一个对象的新引用。如果迭代已经结束,则tp_iternext可以返回NULL而不设置任何异常,或者可以将StopIteration设置为另外返回NULL;避免异常可以产生更好的性能。如果发生实际错误,tp_iternext应该始终设置一个异常并返回NULL

3.6. 参考支持薄弱

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

See also

weakref模块的文档。

为了使对象可弱引用,扩展类型必须做两件事:

  • 在专用于弱引用机制的 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需要清除所有弱引用(pass调用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_以及所需的Function(例如tp_richcompare)。您将找到要实现的Function的示例。

当需要验证对象是否为所实现类型的具体实例时,请使用PyObject_TypeCheck()函数。它的用法示例可能类似于以下内容:

if (!PyObject_TypeCheck(some_object, &MyType)) {
    PyErr_SetString(PyExc_TypeError, "arg #1 not a mything");
    return NULL;
}

See also