2. 定义新类型 — Python 文档

来自菜鸟教程
Python/docs/2.7/extending/newtypes
跳转至:导航、​搜索

2. 定义新类型

正如上一章所提到的,Python 允许扩展模块的作者定义可以从 Python 代码中操作的新类型,就像核心 Python 中的字符串和列表一样。

这并不难; 所有扩展类型的代码都遵循一个模式,但在开始之前,您需要了解一些细节。

笔记

在 Python 2.2 中,定义新类型的方式发生了巨大的变化(并且变得更好)。 本文档记录了如何为 Python 2.2 及更高版本定义新类型。 如果您需要支持旧版本的 Python,则需要参考 本文档的旧版本


2.1. 基础知识

Python 运行时将所有 Python 对象视为 PyObject* 类型的变量。 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_compare */
    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 PyMethodDef noddy_methods[] = {
    {NULL}  /* Sentinel */
};

#ifndef PyMODINIT_FUNC  /* declarations for DLL import/export */
#define PyMODINIT_FUNC void
#endif
PyMODINIT_FUNC
initnoddy(void) 
{
    PyObject* m;

    noddy_NoddyType.tp_new = PyType_GenericNew;
    if (PyType_Ready(&noddy_NoddyType) < 0)
        return;

    m = Py_InitModule3("noddy", noddy_methods,
                       "Example module that creates an extension type.");

    Py_INCREF(&noddy_NoddyType);
    PyModule_AddObject(m, "Noddy", (PyObject *)&noddy_NoddyType);
}

现在一下子就明白了很多,但希望上一章的内容看起来很熟悉。

第一个是新的:

typedef struct {
    PyObject_HEAD
} noddy_NoddyObject;

这就是 Noddy 对象将包含的内容——在这种情况下,只不过是每个 Python 对象包含的内容,即引用计数和指向类型对象的指针。 这些是 PyObject_HEAD 宏引入的字段。 宏的原因是标准化布局并在调试版本中启用特殊的调试字段。 注意PyObject_HEAD宏后面没有分号; 一个包含在宏定义中。 小心不小心添加了一个; 习惯上很容易做到,您的编译器可能不会抱怨,但其他人可能会抱怨! (在 Windows 上,已知 MSVC 将此称为错误并拒绝编译代码。)

作为对比,我们来看看标准 Python 整数的相应定义:

typedef struct {
    PyObject_HEAD
    long ob_ival;
} PyIntObject;

继续前进,我们来到了紧要关头——类型对象。

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_compare */
    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 */
};

现在,如果您在 object.h 中查找 PyTypeObject 的定义,您会发现它具有比上述定义更多的字段。 其余字段将由 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 <module>
TypeError: cannot add type "noddy.Noddy" to string

请注意,名称是一个带点的名称,包括模块名称和模块内的类型名称。 本例中的模块为noddy,类型为Noddy,因此我们将类型名称设置为noddy.Noddy。 使用无点名称的一个副作用是 pydoc 文档工具不会在模块文档中列出新类型。

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 版本定义的所有成员。

我们为 tp_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,所以我们稍后会讨论它们——这是后面的部分!

除了 initnoddy() 中的一些代码之外,文件中的其他所有内容都应该很熟悉:

if (PyType_Ready(&noddy_NoddyType) < 0)
    return;

这将初始化 Noddy 类型,归档多个成员,包括我们最初设置为 NULLob_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 — 您应该能够 import noddy 并使用 Noddy 对象。

那没那么难,是吗?

当然,目前的 Noddy 类型相当无趣。 它没有数据,也不做任何事情。 它甚至不能被子类化。

2.1.1. 向 Basic 示例添加数据和方法

让我们扩展基本示例以添加一些数据和方法。 让我们也使该类型可用作基类。 我们将创建一个新模块 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 = PyString_FromString("");
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }

        self->last = PyString_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)
{
    static PyObject *format = NULL;
    PyObject *args, *result;

    if (format == NULL) {
        format = PyString_FromString("%s %s");
        if (format == NULL)
            return NULL;
    }

    if (self->first == NULL) {
        PyErr_SetString(PyExc_AttributeError, "first");
        return NULL;
    }

    if (self->last == NULL) {
        PyErr_SetString(PyExc_AttributeError, "last");
        return NULL;
    }

    args = Py_BuildValue("OO", self->first, self->last);
    if (args == NULL)
        return NULL;

    result = PyString_Format(format, args);
    Py_DECREF(args);

    return result;
}

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_compare */
    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 PyMethodDef module_methods[] = {
    {NULL}  /* Sentinel */
};

#ifndef PyMODINIT_FUNC  /* declarations for DLL import/export */
#define PyMODINIT_FUNC void
#endif
PyMODINIT_FUNC
initnoddy2(void)
{
    PyObject* m;

    if (PyType_Ready(&NoddyType) < 0)
        return;

    m = Py_InitModule3("noddy2", module_methods,
                       "Example module that creates an extension type.");

    if (m == NULL)
        return;

    Py_INCREF(&NoddyType);
    PyModule_AddObject(m, "Noddy", (PyObject *)&NoddyType);
}

此版本的模块有许多更改。

我们添加了一个额外的包括:

#include <structmember.h>

这包括提供我们用来处理属性的声明,如稍后所述。

Noddy 对象结构的名称已缩短为 Noddy。 类型对象名称已缩短为 NoddyType

Noddy 类型现在具有三个数据属性,firstlastnumberfirstlast 变量是包含名字和姓氏的 Python 字符串。 number 属性是一个整数。

对象结构会相应更新:

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() 是因为 firstlast 成员可能是 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 = PyString_FromString("");
        if (self->first == NULL)
          {
            Py_DECREF(self);
            return NULL;
          }

        self->last = PyString_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”的论文。 实现新方法的一个原因是确保实例变量的初始值。 在这种情况下,我们使用新方法来确保成员 firstlast 的初始值不是 NULL。 如果我们不关心初始值是否为 NULL,我们可以像以前一样使用 PyType_GenericNew() 作为我们的新方法。 PyType_GenericNew() 将所有实例变量成员初始化为 NULL

new 方法是一个静态方法,它传递正在实例化的类型和调用该类型时传递的任何参数,并返回创建的新对象。 新方法总是接受位置和关键字参数,但它们经常忽略参数,将参数处理留给初始化方法。 请注意,如果类型支持子类化,则传递的类型可能不是正在定义的类型。 新方法调用 tp_alloc 槽来分配内存。 我们不会自己填充 tp_alloc 插槽。 而是 PyType_Ready() 通过从我们的基类继承它来为我们填充它,默认情况下它是 object。 大多数类型使用默认分配。

笔记

如果您正在创建一个合作社 tp_new(一个调用基本类型的 tp_new__new__()),您必须 not 尝试确定在运行时使用方法解析顺序调用什么方法。 始终静态地确定您要调用的类型,并直接调用其 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__() 方法。 它用于在创建对象后对其进行初始化。 与新方法不同,我们不能保证调用初始化程序。 在取消对象时不会调用初始化程序,它可以被覆盖。 我们的初始化程序接受参数来为我们的实例提供初始值。 初始化程序总是接受位置和关键字参数。

初始化器可以被多次调用。 任何人都可以在我们的对象上调用 __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 */

每个成员定义都有一个成员名称、类型、偏移量、访问标志和文档字符串。 有关详细信息,请参阅下面的 通用属性管理 部分。

这种方法的一个缺点是它没有提供一种方法来限制可以分配给 Python 属性的对象类型。 我们希望名字和姓氏是字符串,但可以分配任何 Python 对象。 此外,可以删除属性,将 C 指针设置为 NULL。 即使我们可以确保成员被初始化为非 NULL 值,如果属性被删除,成员可以设置为 NULL

我们定义了一个方法,name(),它将对象名称输出为名字和姓氏的串联。

static PyObject *
Noddy_name(Noddy* self)
{
    static PyObject *format = NULL;
    PyObject *args, *result;

    if (format == NULL) {
        format = PyString_FromString("%s %s");
        if (format == NULL)
            return NULL;
    }

    if (self->first == NULL) {
        PyErr_SetString(PyExc_AttributeError, "first");
        return NULL;
    }

    if (self->last == NULL) {
        PyErr_SetString(PyExc_AttributeError, "last");
        return NULL;
    }

    args = Py_BuildValue("OO", self->first, self->last);
    if (args == NULL)
        return NULL;

    result = PyString_Format(format, args);
    Py_DECREF(args);

    return result;
}

该方法被实现为一个 C 函数,它采用 Noddy(或 Noddy 子类)实例作为第一个参数。 方法总是将一个实例作为第一个参数。 方法通常也接受位置参数和关键字参数,但在这种情况下,我们不接受任何参数,也不需要接受位置参数元组或关键字参数字典。 此方法等效于 Python 方法:

def name(self):
   return "%s %s" % (self.first, self.last)

请注意,我们必须检查我们的 firstlast 成员是 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*/

我们将 initnoddy() 重命名为 initnoddy2() 并更新传递给 Py_InitModule3() 的模块名称。

最后,我们更新 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. 提供对数据属性的更精细控制

在本节中,我们将更好地控制如何在 Noddy 示例中设置 firstlast 属性。 在我们模块的先前版本中,实例变量 firstlast 可以设置为非字符串值甚至删除。 我们要确保这些属性始终包含字符串。

#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 = PyString_FromString("");
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }

        self->last = PyString_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 (! PyString_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 (! PyString_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)
{
    static PyObject *format = NULL;
    PyObject *args, *result;

    if (format == NULL) {
        format = PyString_FromString("%s %s");
        if (format == NULL)
            return NULL;
    }

    args = Py_BuildValue("OO", self->first, self->last);
    if (args == NULL)
        return NULL;

    result = PyString_Format(format, args);
    Py_DECREF(args);

    return result;
}

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_compare */
    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 PyMethodDef module_methods[] = {
    {NULL}  /* Sentinel */
};

#ifndef PyMODINIT_FUNC  /* declarations for DLL import/export */
#define PyMODINIT_FUNC void
#endif
PyMODINIT_FUNC
initnoddy3(void)
{
    PyObject* m;

    if (PyType_Ready(&NoddyType) < 0)
        return;

    m = Py_InitModule3("noddy3", module_methods,
                       "Example module that creates an extension type.");

    if (m == NULL)
        return;

    Py_INCREF(&NoddyType);
    PyModule_AddObject(m, "Noddy", (PyObject *)&NoddyType);
}

为了更好地控制 firstlast 属性,我们将使用自定义的 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 (! PyString_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;
}

getter 函数传递了一个 Noddy 对象和一个“闭包”,即空指针。 在这种情况下,闭包将被忽略。 (闭包支持将定义数据传递给 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;
}

通过这些更改,我们可以确保 firstlast 成员永远不会是 NULL,因此我们可以在几乎所有情况下删除对 NULL 值的检查. 这意味着大部分 Py_XDECREF() 调用可以转换为 Py_DECREF() 调用。 我们唯一不能改变这些调用的地方是在解除分配器中,在那里这些成员的初始化有可能在构造函数中失败。

我们还在初始化函数中重命名了模块初始化函数和模块名称,就像我们之前所做的那样,我们在 setup.py 文件中添加了一个额外的定义。


2.1.3. 支持循环垃圾回收

Python 有一个循环垃圾收集器,即使它们的引用计数不为零,它也可以识别不需要的对象。 当对象参与循环时,就会发生这种情况。 例如,考虑:

>>> l = []
>>> l.append(l)
>>> del l

在本例中,我们创建了一个包含自身的列表。 当我们删除它时,它仍然有一个来自它自己的引用。 它的引用计数不会降到零。 幸运的是,Python 的循环垃圾收集器最终会发现列表是垃圾并释放它。

Noddy 示例的第二个版本中,我们允许将任何类型的对象存储在 firstlast 属性 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)
{
    PyObject_GC_UnTrack(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 = PyString_FromString("");
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }

        self->last = PyString_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)
{
    static PyObject *format = NULL;
    PyObject *args, *result;

    if (format == NULL) {
        format = PyString_FromString("%s %s");
        if (format == NULL)
            return NULL;
    }

    if (self->first == NULL) {
        PyErr_SetString(PyExc_AttributeError, "first");
        return NULL;
    }

    if (self->last == NULL) {
        PyErr_SetString(PyExc_AttributeError, "last");
        return NULL;
    }

    args = Py_BuildValue("OO", self->first, self->last);
    if (args == NULL)
        return NULL;

    result = PyString_Format(format, args);
    Py_DECREF(args);

    return result;
}

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_compare */
    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 PyMethodDef module_methods[] = {
    {NULL}  /* Sentinel */
};

#ifndef PyMODINIT_FUNC  /* declarations for DLL import/export */
#define PyMODINIT_FUNC void
#endif
PyMODINIT_FUNC
initnoddy4(void)
{
    PyObject* m;

    if (PyType_Ready(&NoddyType) < 0)
        return;

    m = Py_InitModule3("noddy4", module_methods,
                       "Example module that creates an extension type.");

    if (m == NULL)
        return;

    Py_INCREF(&NoddyType);
    PyModule_AddObject(m, "Noddy", (PyObject *)&NoddyType);
}

遍历方法提供对可以参与循环的子对象的访问:

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() 函数将子对象和传递给遍历方法的额外参数 arg 作为参数。 它返回一个整数值,如果它不为零,则必须返回该值。

Python 2.4 及更高版本提供 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;
}

笔记

请注意,tp_traverse 实现必须准确命名其参数 visitarg,以便使用 Py_VISIT()。 这是为了鼓励这些无聊的实现之间的一致性。


我们还需要提供一种方法来清除任何可以参与循环的子对象。

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

注意 Noddy_clear() 中临时变量的使用。 我们使用临时变量,以便我们可以在减少其引用计数之前将每个成员设置为 NULL。 我们这样做是因为,正如前面所讨论的,如果引用计数降为零,我们可能会导致调用回对象的代码运行。 此外,因为我们现在支持垃圾回收,所以我们也不得不担心正在运行的代码会触发垃圾回收。 如果运行垃圾收集,我们的 tp_traverse 处理程序可能会被调用。 当成员的引用计数降为零且其值尚未设置为 NULL 时,我们不能冒险调用 Noddy_traverse()

Python 2.4 及更高版本提供了 Py_CLEAR() 来自动小心地减少引用计数。 使用 Py_CLEAR(),可以简化 Noddy_clear() 函数:

static int
Noddy_clear(Noddy *self)
{
    Py_CLEAR(self->first);
    Py_CLEAR(self->last);
    return 0;
}

注意 Noddy_dealloc() 可以通过 __del__ 方法或weakref 回调调用任意函数。 这意味着可以在函数内部触发循环 GC。 由于 GC 假设引用计数不为零,因此我们需要在清除成员之前通过调用 PyObject_GC_UnTrack() 来从 GC 中取消跟踪对象。 这是重新实现的释放器,它使用 PyObject_GC_UnTrack()Noddy_clear()

static void
Noddy_dealloc(Noddy* self)
{
    PyObject_GC_UnTrack(self);
    Noddy_clear(self);
    Py_TYPE(self)->tp_free((PyObject*)self);
}

最后,我们将 Py_TPFLAGS_HAVE_GC 标志添加到类标志中:

Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, /* tp_flags */

差不多就是这样。 如果我们编写了自定义的 tp_alloctp_free 槽,我们需要修改它们以进行循环垃圾收集。 大多数扩展将使用自动提供的版本。


2.1.4. 子类化其他类型

可以创建从现有类型派生的新扩展类型。 从内置类型继承是最容易的,因为扩展可以轻松使用它需要的 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 PyInt_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 = {
    PyVarObject_HEAD_INIT(NULL, 0)
    "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_compare */
    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 */
};

PyMODINIT_FUNC
initshoddy(void)
{
    PyObject *m;

    ShoddyType.tp_base = &PyList_Type;
    if (PyType_Ready(&ShoddyType) < 0)
        return;

    m = Py_InitModule3("shoddy", NULL, "Shoddy module");
    if (m == NULL)
        return;

    Py_INCREF(&ShoddyType);
    PyModule_AddObject(m, "Shoddy", (PyObject *) &ShoddyType);
}

如您所见,源代码与前几节中的 Noddy 示例非常相似。 我们将分解它们之间的主要区别。

typedef struct {
    PyListObject list;
    int state;
} Shoddy;

派生类型对象的主要区别在于基类型的对象结构必须是第一个值。 基本类型将在其结构的开头包含 PyObject_HEAD()

当 Python 对象是 Shoddy 实例时,其 PyObject* 指针可以安全地转换为 PyListObject*Shoddy*

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__ 方法。

在使用自定义 newdealloc 方法编写类型时,此模式很重要。 new 方法实际上不应该为具有 tp_alloc 的对象创建内存,这将由基类在调用其 tp_new 时处理。

在为 Shoddy 类型填写 PyTypeObject() 时,您会看到一个用于 tp_base() 的插槽。 由于跨平台编译器问题,您不能直接使用 PyList_Type(); 填充该字段; 它可以稍后在模块的 init() 函数中完成。

PyMODINIT_FUNC
initshoddy(void)
{
    PyObject *m;

    ShoddyType.tp_base = &PyList_Type;
    if (PyType_Ready(&ShoddyType) < 0)
        return;

    m = Py_InitModule3("shoddy", NULL, "Shoddy module");
    if (m == NULL)
        return;

    Py_INCREF(&ShoddyType);
    PyModule_AddObject(m, "Shoddy", (PyObject *) &ShoddyType);
}

在调用 PyType_Ready() 之前,类型结构必须填充 tp_base 槽。 当我们派生一个新类型时,没有必要用 PyType_GenericNew() 填充 tp_alloc 槽——来自基类型的分配函数将被继承。

之后,调用 PyType_Ready() 并将类型对象添加到模块与基本的 Noddy 示例相同。


2.2. 类型方法

本节旨在快速浏览您可以实现的各种类型方法以及它们的作用。

这是 PyTypeObject 的定义,省略了一些仅在调试版本中使用的字段:

typedef struct _typeobject {
    PyObject_VAR_HEAD
    char *tp_name; /* For printing, in format "<module>.<name>" */
    int tp_basicsize, tp_itemsize; /* For allocation */

    /* Methods to implement standard operations */

    destructor tp_dealloc;
    printfunc tp_print;
    getattrfunc tp_getattr;
    setattrfunc tp_setattr;
    cmpfunc tp_compare;
    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 */
    long tp_flags;

    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 */
    long tp_weaklistoffset;

    /* Added in release 2.2 */
    /* 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;
    long 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;

} PyTypeObject;

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

正如您现在可能期望的那样,我们将对此进行讨论并提供有关各种处理程序的更多信息。 我们不会按照它们在结构中定义的顺序进行,因为有很多影响字段顺序的历史包袱; 确保您的类型初始化使字段保持正确的顺序! 通常最容易找到包含您需要的所有字段的示例(即使它们已初始化为 0),然后更改值以适合您的新类型。

char *tp_name; /* For printing */

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

int tp_basicsize, tp_itemsize; /* For allocation */

这些字段告诉运行时在创建这种类型的新对象时要分配多少内存。 Python 对可变长度结构(想想:字符串、列表)有一些内置支持,这就是 tp_itemsize 字段的用武之地。 这将在稍后处理。

char *tp_doc;

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

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

2.2.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;
        int have_error = PyErr_Occurred() ? 1 : 0;

        if (have_error)
            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);

        if (have_error)
            PyErr_Restore(err_type, err_value, err_traceback);

        Py_DECREF(self->my_callback);
    }
    Py_TYPE(obj)->tp_free((PyObject*)self);
}

2.2.2. 对象展示

在 Python 中,有三种方法可以生成对象的文本表示:repr() 函数(或等效的反引号语法)、str() 函数和打印语句。 对于大多数对象,print 语句等效于 str() 函数,但如有必要,可以将特殊情况打印到 FILE*; 只有当效率被确定为问题并且分析表明创建要写入文件的临时字符串对象太昂贵时才应该这样做。

这些处理程序都是可选的,大多数类型最多需要实现 tp_strtp_repr 处理程序。

reprfunc tp_repr;
reprfunc tp_str;
printfunc tp_print;

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

static PyObject *
newdatatype_repr(newdatatypeobject * obj)
{
    return PyString_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 PyString_FromFormat("Stringified_newdatatype{{size:\%d}}",
                               obj->obj_UnderlyingDatatypePtr->size);
}

每当 Python 需要“打印”该类型的实例时,都会调用 print 函数。 例如,如果 'node' 是 TreeNode 类型的实例,那么在 Python 代码调用时会调用打印函数:

print node

有一个标志参数和一个标志 Py_PRINT_RAW,它建议您在打印时不使用字符串引号,也可能不解释转义序列。

打印函数接收一个文件对象作为参数。 您可能希望写入该文件对象。

这是一个示例打印功能:

static int
newdatatype_print(newdatatypeobject *obj, FILE *fp, int flags)
{
    if (flags & Py_PRINT_RAW) {
        fprintf(fp, "<{newdatatype object--size: %d}>",
                obj->obj_UnderlyingDatatypePtr->size);
    }
    else {
        fprintf(fp, "\"<{newdatatype object--size: %d}>\"",
                obj->obj_UnderlyingDatatypePtr->size);
    }
    return 0;
}

2.2.3. 属性管理

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

Python 支持两对属性处理程序; 支持属性的类型只需要实现一对函数。 不同之处在于一对将属性名称作为 char*,而另一对接受 PyObject*。 为了实现的方便,每种类型都可以使用更有意义的一对。

getattrfunc  tp_getattr;        /* char * version */
setattrfunc  tp_setattr;
/* ... */
getattrofunc tp_getattrofunc;   /* PyObject * version */
setattrofunc tp_setattrofunc;

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

2.2.3.1. 通用属性管理

2.2 版中的新功能。


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

  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

XXX 需要参考一些关于结构域的统一讨论,与下一节分享。

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

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

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

XXX 需要将其中的一些移到共享部分!

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

常数 意义
READONLY 永远不可写。
RO READONLY 的简写。
READ_RESTRICTED 在受限模式下不可读。
WRITE_RESTRICTED 在受限模式下不可写。
RESTRICTED 在受限模式下不可读或不可写。

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

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


2.2.3.2. 特定于类型的属性管理

为简单起见,这里只展示char*版本; name 参数的类型是接口的 char*PyObject* 风格之间的唯一区别。 此示例有效地执行与上述通用示例相同的操作,但不使用 Python 2.2 中添加的通用支持。 展示这一点的价值有两个方面:它展示了如何以一种可移植到旧版 Python 的方式完成基本的属性管理,并解释了如何调用处理程序函数,以便在您确实需要扩展它们的功能时,你就会明白需要做什么。

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

一种可能的处理方法是 (1) 实现一组函数(例如下面示例中的 newdatatype_getSize()newdatatype_setSize()),(2) 提供列出这些函数的方法表,以及(3) 提供一个 getattr 函数,该函数返回在该表中查找的结果。 方法表使用与类型对象的 tp_methods 字段相同的结构。

下面是一个例子:

static PyMethodDef newdatatype_methods[] = {
    {"getSize", (PyCFunction)newdatatype_getSize, METH_VARARGS,
     "Return the current size."},
    {"setSize", (PyCFunction)newdatatype_setSize, METH_VARARGS,
     "Set the size."},
    {NULL, NULL, 0, NULL}           /* sentinel */
};

static PyObject *
newdatatype_getattr(newdatatypeobject *obj, char *name)
{
    return Py_FindMethod(newdatatype_methods, (PyObject *)obj, name);
}

tp_setattr 处理程序在调用类实例的 __setattr__()__delattr__() 方法时被调用。 当一个属性应该被删除时,第三个参数将是 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. 对象比较

cmpfunc tp_compare;

tp_compare 处理程序在需要比较并且对象未实现与请求的比较匹配的特定丰富比较方法时调用。 (如果已定义并使用 PyObject_Compare()PyObject_Cmp() 函数,或者如果 cmp() 从 Python 使用,则始终使用它。)类似于 __cmp__() 方法。 如果 obj1 小于 obj2,则该函数应该返回 -1,如果它们相等,则返回 0,如果 小于 1 ]obj1 大于 obj2。 (以前允许分别返回小于和大于的任意负整数或正整数;从 Python 2.2 开始,不再允许这样做。 将来,其他返回值可能会被赋予不同的含义。)

tp_compare 处理程序可能会引发异常。 在这种情况下,它应该返回一个负值。 调用者必须使用 PyErr_Occurred() 测试异常。

这是一个示例实现:

static int
newdatatype_compare(newdatatypeobject * obj1, newdatatypeobject * obj2)
{
    long result;

    if (obj1->obj_UnderlyingDatatypePtr->size <
        obj2->obj_UnderlyingDatatypePtr->size) {
        result = -1;
    }
    else if (obj1->obj_UnderlyingDatatypePtr->size >
             obj2->obj_UnderlyingDatatypePtr->size) {
        result = 1;
    }
    else {
        result = 0;
    }
    return result;
}

2.2.5. 抽象协议支持

Python 支持多种 abstract 'protocols;' 为使用这些接口而提供的特定接口记录在 抽象对象层 中。

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

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

如果您希望您的对象能够像数字、序列或映射对象一样工作,那么您可以放置实现 C 类型 PyNumberMethodsPySequenceMethods 的结构的地址, 或 PyMappingMethods,分别。 您可以用适当的值填充此结构。 您可以在 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;

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

这个函数接受三个参数:

  1. arg1 是作为调用对象的数据类型的实例。 如果调用是 obj1('hello'),则 arg1obj1
  2. arg2 是一个包含调用参数的元组。 您可以使用 PyArg_ParseTuple() 来提取参数。
  3. 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 = PyString_FromFormat(
        "Returning -- value: [\%d] arg1: [\%s] arg2: [\%s] arg3: [\%s]\n",
        obj->obj_UnderlyingDatatypePtr->size,
        arg1, arg2, arg3);
    printf("\%s", PyString_AS_STRING(result));
    return result;
}

XXX 这里需要添加一些字段…

/* Added in release 2.2 */
/* Iterators */
getiterfunc tp_iter;
iternextfunc tp_iternext;

这些函数为迭代器协议提供支持。 任何希望支持对其内容进行迭代(可能在迭代期间生成)的对象都必须实现 tp_iter 处理程序。 由 tp_iter 处理程序返回的对象必须同时实现 tp_itertp_iternext 处理程序。 两个处理程序都只接受一个参数,即调用它们的实例,并返回一个新的引用。 在出现错误的情况下,他们应该设置一个异常并返回 NULL

对于表示可迭代集合的对象,tp_iter 处理程序必须返回一个迭代器对象。 迭代器对象负责维护迭代的状态。 对于可以支持多个互不干扰的迭代器的集合(如列表和元组),应该创建并返回一个新的迭代器。 只能迭代一次的对象(通常是由于迭代的副作用)应该通过返回一个新的引用来实现这个处理程序,并且还应该实现 tp_iternext 处理程序。 文件对象就是这种迭代器的一个例子。

迭代器对象应该实现这两个处理程序。 tp_iter 处理程序应该返回对迭代器的新引用(这与只能破坏性迭代的对象的 tp_iter 处理程序相同)。 tp_iternext 处理程序应该返回对迭代中下一个对象的新引用(如果有)。 如果迭代已经结束,可能会返回NULL而不设置异常或者可能会设置StopIteration; 避免异常可以产生稍微更好的性能。 如果实际发生错误,则应设置异常并返回 NULL


2.2.6. 弱引用支持

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 = {
    PyObject_HEAD_INIT(&PyType_Type)
    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. 更多建议

请记住,您可以省略大多数这些函数,在这种情况下,您可以提供 0 作为值。 您必须提供的每个函数都有类型定义。 它们位于 Python 源代码分发版附带的 Python 包含目录中的 object.h 中。

为了了解如何为您的新数据类型实现任何特定方法,请执行以下操作: 下载并解压 Python 源代码分发版。 进入 Objects 目录,然后在 C 源文件中搜索 tp_ 加上您想要的函数(例如,tp_printtp_compare)。 您将找到要实现的功能的示例。

当您需要验证对象是否是您正在实现的类型的实例时,请使用 PyObject_TypeCheck() 函数。 它的使用示例可能如下所示:

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

脚注

1
当我们知道对象是一个基本类型时,这是真的,比如字符串或浮点数。
2
在本例中,我们在 tp_dealloc 处理程序中依赖于此,因为我们的类型不支持垃圾收集。 即使类型支持垃圾回收,也可以进行一些调用以从垃圾回收中“取消跟踪”对象,但是,这些调用是高级调用,此处未涵盖。
3
我们现在知道第一个和最后一个成员是字符串,所以也许我们可以在减少它们的引用计数时不那么小心,但是,我们接受字符串子类的实例。 即使释放普通字符串不会回调到我们的对象中,我们也不能保证释放字符串子类的实例不会回调到我们的对象中。
4
即使在第三个版本中,我们也不能保证避免循环。 字符串子类的实例是允许的,即使普通字符串不允许,字符串子类也可以允许循环。