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 */

    /* Assigned meaning in release 2.0 */
    /* call function for all accessible objects */
    traverseproc tp_traverse;

    /* delete references to contained objects */
    inquiry tp_clear;

    /* Assigned meaning in release 2.1 */
    /* 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 */
    PyMethodDef *tp_methods;
    PyMemberDef *tp_members;
    PyGetSetDef *tp_getset;
    // Strong reference on a heap type, borrowed reference on a static type
    PyTypeObject *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; /* no longer used */
    void *tp_subclasses;  /* for static builtin types this is an index */
    PyObject *tp_weaklist; /* not used for static builtin types */
    destructor tp_del;

    /* Type attribute cache version tag. Added in version 2.6.
     * If zero, the cache is invalid and must be initialized.
     */
    unsigned int tp_version_tag;

    destructor tp_finalize;
    vectorcallfunc tp_vectorcall;

    /* bitset of which type-watchers care about this type */
    unsigned char tp_watched;

    /* Number of tp_version_tag values used.
     * Set to _Py_ATTR_CACHE_UNUSED if the attribute cache is
     * disabled for this type (e.g. due to custom MRO entries).
     * Otherwise, limited to MAX_VERSIONS_PER_CLASS (defined elsewhere).
     */
    uint16_t tp_versions_used;
} 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(PyObject *op)
{
    newdatatypeobject *self = (newdatatypeobject *) op;
    free(self->obj_UnderlyingDatatypePtr);
    Py_TYPE(self)->tp_free(self);
}

如果你的类型支持垃圾回收,析构函数应在清除任何成员字段之前调用 PyObject_GC_UnTrack()

static void
newdatatype_dealloc(PyObject *op)
{
    newdatatypeobject *self = (newdatatypeobject *) op;
    PyObject_GC_UnTrack(op);
    Py_CLEAR(self->other_obj);
    ...
    Py_TYPE(self)->tp_free(self);
}

析构函数的一个重要要求是它不处理任何待处理的异常。这一点很重要,因为析构函数通常在解释器解开 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_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(self)->tp_free(self);
}

备注

在析构函数中可以安全执行的操作是有限制的。首先,如果你的类型支持垃圾回收(使用 tp_traverse 和/或 tp_clear),在调用 tp_dealloc 时,对象的某些成员可能已经被清除或终结。其次,在 tp_dealloc 中,你的对象处于不稳定状态:其引用计数为零。对非平凡对象或 API 的任何调用(如上述示例)都可能最终再次调用 tp_dealloc,导致二次释放和崩溃。

从 Python 3.4 开始,建议不要在 tp_dealloc 中放置任何复杂的终结代码,而是使用新的 tp_finalize 类型方法。

参见

PEP 442 解释了新的终结方案。

3.2. 对象表示

在 Python 中,有两种方法可以生成对象的文本表示:repr() 函数和 str() 函数。(print() 函数只是调用 str()。)这些处理程序都是可选的。

reprfunc tp_repr;
reprfunc tp_str;

tp_repr 处理程序应返回一个字符串对象,其中包含为其调用实例的表示。这是一个简单的示例

static PyObject *
newdatatype_repr(PyObject *op)
{
    newdatatypeobject *self = (newdatatypeobject *) op;
    return PyUnicode_FromFormat("Repr-ified_newdatatype{{size:%d}}",
                                self->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(PyObject *op)
{
    newdatatypeobject *self = (newdatatypeobject *) op;
    return PyUnicode_FromFormat("Stringified_newdatatype{{size:%d}}",
                                self->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() 时,它使用类型对象引用的三个表来创建 描述符,这些描述符放置在类型对象的字典中。每个描述符控制对实例对象一个属性的访问。每个表都是可选的;如果所有三个都为 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;

对于表中的每个条目,将构造一个 描述符 并将其添加到类型中,该描述符将能够从实例结构中提取值。type 字段应包含一个类型代码,如 Py_T_INTPy_T_DOUBLE;该值将用于确定如何将 Python 值与 C 值相互转换。flags 字段用于存储控制属性访问方式的标志:可以将其设置为 Py_READONLY 以防止 Python 代码设置它。

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

tp_methods 表一样,需要一个 ml_name 值为 NULL 的哨兵条目。

3.3.2. 类型特定属性管理

为简单起见,这里只演示 char* 版本;名称参数的类型是 char*PyObject* 接口之间的唯一区别。此示例实际上执行与上述通用示例相同的事情,但未使用 Python 2.2 中添加的通用支持。它解释了如何调用处理程序函数,以便如果你确实需要扩展其功能,你将了解需要做什么。

当对象需要属性查找时,将调用 tp_getattr 处理程序。它在与调用类的 __getattr__() 方法相同的情况下被调用。

下面是一个例子:

static PyObject *
newdatatype_getattr(PyObject *op, char *name)
{
    newdatatypeobject *self = (newdatatypeobject *) op;
    if (strcmp(name, "data") == 0) {
        return PyLong_FromLong(self->data);
    }

    PyErr_Format(PyExc_AttributeError,
                 "'%.100s' object has no attribute '%.400s'",
                 Py_TYPE(self)->tp_name, name);
    return NULL;
}

当调用类实例的 __setattr__()__delattr__() 方法时,将调用 tp_setattr 处理程序。当需要删除属性时,第三个参数将为 NULL。这是一个简单地引发异常的示例;如果这确实是你想要的,则 tp_setattr 处理程序应设置为 NULL

static int
newdatatype_setattr(PyObject *op, 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_EQPy_NEPy_LEPy_GEPy_LTPy_GT 之一。它应根据指定的运算符比较这两个对象,并在比较成功时返回 Py_TruePy_False,返回 Py_NotImplemented 表示未实现比较且应尝试另一个对象的比较方法,或在设置异常时返回 NULL

这是一个示例实现,对于内部指针大小相等则被认为相等的数据类型

static PyObject *
newdatatype_richcmp(PyObject *lhs, PyObject *rhs, int op)
{
    newdatatypeobject *obj1 = (newdatatypeobject *) lhs;
    newdatatypeobject *obj2 = (newdatatypeobject *) rhs;
    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;
    return Py_NewRef(result);
 }

3.5. 抽象协议支持

Python 支持各种 抽象 “协议”;使用这些接口提供的特定接口已在抽象对象层中进行了说明。

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

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

如果你希望你的对象能够像数字、序列或映射对象一样,那么你需要放置实现 C 类型 PyNumberMethodsPySequenceMethodsPyMappingMethods 的结构地址。你需要用适当的值填充此结构。你可以在 Python 源代码分发的 Objects 目录中找到每个用途的示例。

hashfunc tp_hash;

此函数,如果你选择提供它,应返回数据类型实例的哈希值。这是一个简单的示例

static Py_hash_t
newdatatype_hash(PyObject *op)
{
    newdatatypeobject *self = (newdatatypeobject *) op;
    Py_hash_t result;
    result = self->some_size + 32767 * self->some_number;
    if (result == -1) {
        result = -2;
    }
    return result;
}

Py_hash_t 是一个带符号整数类型,其宽度随平台而异。从 tp_hash 返回 -1 表示错误,这就是为什么在哈希计算成功时应小心避免返回它,如上所示。

ternaryfunc tp_call;

当你的数据类型的一个实例被“调用”时,例如,如果 obj1 是你的数据类型的一个实例,并且 Python 脚本包含 obj1('hello'),则会调用 tp_call 处理程序。

此函数接受三个参数

  1. self 是作为调用主体的该数据类型的实例。如果调用是 obj1('hello'),那么 self 就是 obj1

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

  3. kwds 是一个已传递的关键字参数字典。如果它不为 NULL 并且你支持关键字参数,请使用 PyArg_ParseTupleAndKeywords() 来提取参数。如果你不想支持关键字参数且它不为 NULL,请引发 TypeError 并附带一条消息,说明不支持关键字参数。

这是一个简单的 tp_call 实现

static PyObject *
newdatatype_call(PyObject *op, PyObject *args, PyObject *kwds)
{
    newdatatypeobject *self = (newdatatypeobject *) op;
    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",
        self->obj_UnderlyingDatatypePtr->size,
        arg1, arg2, arg3);
    return result;
}
/* Iterators */
getiterfunc tp_iter;
iternextfunc tp_iternext;

这些函数提供对迭代器协议的支持。两个处理程序都只接受一个参数,即它们被调用的实例,并返回一个新的引用。如果发生错误,它们应该设置一个异常并返回 NULLtp_iter 对应于 Python 的 __iter__() 方法,而 tp_iternext 对应于 Python 的 __next__() 方法。

任何 可迭代 对象都必须实现 tp_iter 处理程序,该处理程序必须返回一个 迭代器 对象。这里的准则与 Python 类相同

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

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

任何 迭代器 对象都应同时实现 tp_itertp_iternext。迭代器的 tp_iter 处理程序应返回对迭代器的新引用。其 tp_iternext 处理程序应返回对迭代中下一个对象的新引用(如果存在)。如果迭代已到达末尾,tp_iternext 可以返回 NULL 而不设置异常,或者它可以在返回 NULL 之外设置 StopIteration;避免异常可以略微提高性能。如果发生实际错误,tp_iternext 应始终设置异常并返回 NULL

3.6. 弱引用支持

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

参见

weakref 模块的文档。

为了使对象可弱引用,扩展类型必须设置 tp_flags 字段的 Py_TPFLAGS_MANAGED_WEAKREF 位。传统的 tp_weaklistoffset 字段应保留为零。

具体来说,静态声明的类型对象将如下所示

static PyTypeObject TrivialType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    /* ... other members omitted for brevity ... */
    .tp_flags = Py_TPFLAGS_MANAGED_WEAKREF | ...,
};

唯一进一步的补充是 tp_dealloc 需要清除任何弱引用(通过调用 PyObject_ClearWeakRefs()

static void
Trivial_dealloc(PyObject *op)
{
    /* Clear weakrefs first before calling any destructors */
    PyObject_ClearWeakRefs(op);
    /* ... remainder of destruction code omitted for brevity ... */
    Py_TYPE(op)->tp_free(op);
}

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://pythonlang.cn/downloads/source/

GitHub 上的 CPython 项目,CPython 源代码在此处开发。

https://github.com/python/cpython