Документация Python неофициальный перевод
Содержание страницы

2. Определение типов расширений: учебное пособиеDefining Extension Types: Tutorial

Python позволяет разработчику модуля расширения на C определять новые типы, которыми можно манипулировать из кода Python, во многом как встроенные типы str и list. Код для всех типов расширений следует определённому шаблону, но есть некоторые детали, которые необходимо понять, прежде чем приступить к работе. Этот документ является вводным руководством по данной теме.

2.1. ОсновыThe Basics

Среда выполнения CPython видит все объекты Python как переменные типа PyObject*, который служит «базовым типом» для всех объектов Python. Сама структура PyObject содержит только счётчик ссылок объекта и указатель на его «объект типа». В этом и заключается суть: объект типа определяет, какие (C) функции вызывает интерпретатор, когда, например, выполняется поиск атрибута у объекта, вызов метода или умножение на другой объект. Эти C-функции называются «методами типа».

Итак, чтобы определить новый тип расширения, необходимо создать новый объект типа.

Такие вещи можно объяснить только на примере, поэтому вот минимальный, но полный модуль, определяющий новый тип с именем Custom внутри C-модуля расширения custom:

Примечание

То, что мы показываем здесь, – это традиционный способ определения статических типов расширений. Он должен быть достаточным для большинства случаев. C API также позволяет определять типы расширений, размещаемые в куче, с помощью функции PyType_FromSpec(), которая не рассматривается в этом руководстве.

#define PY_SSIZE_T_CLEAN
#include <Python.h>

typedef struct {
    PyObject_HEAD
    /* Сюда помещаются поля, специфичные для типа. */
} CustomObject;

static PyTypeObject CustomType = {
    .ob_base = PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom.Custom",
    .tp_doc = PyDoc_STR("Custom objects"),
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_new = PyType_GenericNew,
};

static PyModuleDef custommodule = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "custom",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};

PyMODINIT_FUNC
PyInit_custom(void)
{
    PyObject *m;
    if (PyType_Ready(&CustomType) < 0)
        return NULL;

    m = PyModule_Create(&custommodule);
    if (m == NULL)
        return NULL;

    Py_INCREF(&CustomType);
    if (PyModule_AddObject(m, "Custom", (PyObject *) &CustomType) < 0) {
        Py_DECREF(&CustomType);
        Py_DECREF(m);
        return NULL;
    }

    return m;
}

Это довольно много для одномоментного восприятия, но, надеюсь, некоторые части покажутся знакомыми из предыдущей главы. Этот файл определяет три вещи:

  1. Что содержит Custom объект: это структура CustomObject, которая выделяется один раз для каждого экземпляра Custom.

  2. Как ведёт себя Custom тип: это структура CustomType, которая определяет набор флагов и указателей на функции, которые интерпретатор проверяет при запросе определённых операций.

  3. Как инициализировать модуль custom: это PyInit_custom функция и связанная с ней структура custommodule.

Первая часть:

typedef struct {
    PyObject_HEAD
} CustomObject;

Вот что будет содержать объект Custom. PyObject_HEAD является обязательным в начале каждой структуры объекта и определяет поле с именем ob_base типа PyObject, содержащее указатель на объект типа и счётчик ссылок (к ним можно получить доступ с помощью макросов Py_TYPE и Py_REFCNT соответственно). Смысл макроса – абстрагироваться от расположения полей и разрешить дополнительные поля в отладочных сборках.

Примечание

В приведённом выше коде нет точки с запятой после макроса PyObject_HEAD. Будьте осторожны, чтобы случайно её не добавить: некоторые компиляторы выдадут ошибку.

Конечно, объекты обычно хранят дополнительные данные помимо стандартного шаблона PyObject_HEAD; например, вот определение для стандартных чисел с плавающей запятой Python:

typedef struct {
    PyObject_HEAD
    double ob_fval;
} PyFloatObject;

Вторая часть – это определение объекта типа.

static PyTypeObject CustomType = {
    .ob_base = PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom.Custom",
    .tp_doc = PyDoc_STR("Custom objects"),
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_new = PyType_GenericNew,
};

Примечание

Рекомендуется использовать именованные инициализаторы в стиле C99, как указано выше, чтобы не перечислять все поля PyTypeObject, которые не важны, а также не беспокоиться о порядке объявления полей.

Фактическое определение PyTypeObject в object.h содержит гораздо больше полей, чем приведённое выше определение. Оставшиеся поля будут заполнены нулями компилятором C, и обычно принято не указывать их явно, если они не нужны.

Мы разберём его, поле за полем:

.ob_base = PyVarObject_HEAD_INIT(NULL, 0)

Эта строка является обязательным шаблоном для инициализации поля ob_base, упомянутого выше.

.tp_name = "custom.Custom",

Имя нашего типа. Оно будет отображаться в стандартном текстовом представлении наших объектов и в некоторых сообщениях об ошибках, например:

>>> "" + custom.Custom()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "custom.Custom") to str

Обратите внимание, что имя является составным (с точками) и включает как имя модуля, так и имя типа внутри модуля. В данном случае модуль – custom, а тип – Custom, поэтому мы устанавливаем имя типа custom.Custom. Использование реального составного пути импорта важно для совместимости вашего типа с модулями pydoc и pickle.

.tp_basicsize = sizeof(CustomObject),
.tp_itemsize = 0,

Это нужно, чтобы Python знал, сколько памяти выделять при создании новых экземпляров Custom. tp_itemsize используется только для объектов переменного размера, в остальных случаях должно быть равно нулю.

Примечание

Если вы хотите, чтобы ваш тип можно было наследовать из Python, и ваш тип имеет тот же tp_basicsize, что и его базовый тип, у вас могут возникнуть проблемы с множественным наследованием. Подкласс вашего типа на Python должен будет указать ваш тип первым в своём __bases__, иначе он не сможет вызвать метод __new__() вашего типа без ошибки. Вы можете избежать этой проблемы, обеспечив большее значение для tp_basicsize вашего типа по сравнению с его базовым типом. В большинстве случаев это будет верно, потому что либо ваш базовый тип будет object, либо вы будете добавлять члены данных в базовый тип, тем самым увеличивая его размер.

Мы устанавливаем флаги класса в Py_TPFLAGS_DEFAULT.

.tp_flags = Py_TPFLAGS_DEFAULT,

Все типы должны включать эту константу в свои флаги. Она включает все члены, определённые до Python 3.3 как минимум. Если вам нужны дополнительные члены, вам потребуется выполнить OR с соответствующими флагами.

Мы предоставляем строку документации для типа в tp_doc.

.tp_doc = PyDoc_STR("Custom objects"),

Чтобы разрешить создание объектов, мы должны предоставить обработчик tp_new. Это эквивалент метода Python __new__(), но должен быть указан явно. В данном случае мы можем просто использовать реализацию по умолчанию, предоставляемую API-функцией PyType_GenericNew().

.tp_new = PyType_GenericNew,

Всё остальное в файле должно быть знакомым, за исключением некоторого кода в PyInit_custom():

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

Это инициализирует тип Custom, заполняя ряд членов подходящими значениями по умолчанию, включая ob_type, который мы изначально установили в NULL.

Py_INCREF(&CustomType);
if (PyModule_AddObject(m, "Custom", (PyObject *) &CustomType) < 0) {
    Py_DECREF(&CustomType);
    Py_DECREF(m);
    return NULL;
}

Это добавляет тип в словарь модуля. Это позволяет нам создавать экземпляры Custom вызовом класса Custom:

>>> import custom
>>> mycustom = custom.Custom()

Вот и всё! Осталось только собрать его; поместите приведённый выше код в файл с именем custom.c,

[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[project]
name = "custom"
version = "1"

в файл с именем pyproject.toml, и

from setuptools import Extension, setup
setup(ext_modules=[Extension("custom", ["custom.c"])])

в файл с именем setup.py; затем введите

$ python -m pip install .

в оболочке должен создать файл custom.so в подкаталоге и установить его; теперь запустите Python – вы сможете import custom и поиграть с объектами Custom.

Было не так сложно, правда?

Конечно, текущий тип Custom довольно неинтересен. У него нет данных, и он ничего не делает. Его даже нельзя унаследовать.

2.2. Добавление данных и методов к базовому примеруAdding data and methods to the Basic example

Давайте расширим базовый пример, добавив некоторые данные и методы. Также сделаем тип пригодным для использования в качестве базового класса. Мы создадим новый модуль custom2, который добавляет эти возможности:

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stddef.h> /* для offsetof() */

typedef struct {
    PyObject_HEAD
    PyObject *first; /* имя */
    PyObject *last;  /* фамилия */
    int number;
} CustomObject;

static void
Custom_dealloc(CustomObject *self)
{
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free((PyObject *) self);
}

static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = PyUnicode_FromString("");
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = PyUnicode_FromString("");
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;

    if (first) {
        Py_XSETREF(self->first, Py_NewRef(first));
    }
    if (last) {
        Py_XSETREF(self->last, Py_NewRef(last));
    }
    return 0;
}

static PyMemberDef Custom_members[] = {
    {"first", Py_T_OBJECT_EX, offsetof(CustomObject, first), 0,
     "first name"},
    {"last", Py_T_OBJECT_EX, offsetof(CustomObject, last), 0,
     "last name"},
    {"number", Py_T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Страж */
};

static PyObject *
Custom_name(CustomObject *self, PyObject *Py_UNUSED(ignored))
{
    if (self->first == NULL) {
        PyErr_SetString(PyExc_AttributeError, "first");
        return NULL;
    }
    if (self->last == NULL) {
        PyErr_SetString(PyExc_AttributeError, "last");
        return NULL;
    }
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}

static PyMethodDef Custom_methods[] = {
    {"name", (PyCFunction) Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* Страж */
};

static PyTypeObject CustomType = {
    .ob_base = PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom2.Custom",
    .tp_doc = PyDoc_STR("Custom objects"),
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_new = Custom_new,
    .tp_init = (initproc) Custom_init,
    .tp_dealloc = (destructor) Custom_dealloc,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
};

static PyModuleDef custommodule = {
    .m_base =PyModuleDef_HEAD_INIT,
    .m_name = "custom2",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};

PyMODINIT_FUNC
PyInit_custom2(void)
{
    PyObject *m;
    if (PyType_Ready(&CustomType) < 0)
        return NULL;

    m = PyModule_Create(&custommodule);
    if (m == NULL)
        return NULL;

    if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
        Py_DECREF(m);
        return NULL;
    }

    return m;
}

Эта версия модуля содержит ряд изменений.

Тип Custom теперь имеет три атрибута данных в своей C-структуре: first, last и number. Переменные first и last являются строками Python, содержащими имя и фамилию. Атрибут number – это целое число C.

Структура объекта обновляется соответствующим образом:

typedef struct {
    PyObject_HEAD
    PyObject *first; /* имя */
    PyObject *last;  /* фамилия */
    int number;
} CustomObject;

Поскольку теперь у нас есть данные для управления, мы должны быть более внимательны к выделению и освобождению памяти объектов. Как минимум, нам нужен метод освобождения:

static void
Custom_dealloc(CustomObject *self)
{
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free((PyObject *) self);
}

который присваивается члену tp_dealloc:

.tp_dealloc = (destructor) Custom_dealloc,

Этот метод сначала сбрасывает счётчики ссылок двух атрибутов Python. Py_XDECREF() корректно обрабатывает случай, когда его аргумент равен NULL (что может произойти, если tp_new не выполнился до конца). Затем он вызывает член tp_free типа объекта (вычисленный с помощью Py_TYPE(self)) для освобождения памяти объекта. Обратите внимание, что тип объекта может не быть CustomType, поскольку объект может быть экземпляром подкласса.

Примечание

Явное приведение к destructor выше необходимо, потому что мы определили Custom_dealloc как принимающий аргумент CustomObject *, но указатель на функцию tp_dealloc ожидает получения аргумента PyObject *. В противном случае компилятор выдаст предупреждение. Это объектно-ориентированный полиморфизм в C!

Мы хотим убедиться, что имя и фамилия инициализируются пустыми строками, поэтому мы предоставляем реализацию tp_new:

static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = PyUnicode_FromString("");
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = PyUnicode_FromString("");
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

и устанавливаем его в член tp_new:

.tp_new = Custom_new,

Обработчик tp_new отвечает за создание (в отличие от инициализации) объектов типа. Он предоставляется в Python как метод __new__(). Не требуется определять член tp_new, и действительно, многие расширения типов просто повторно используют PyType_GenericNew(), как это сделано в первой версии типа Custom выше. В этом случае мы используем обработчик tp_new для инициализации атрибутов first и last значениями по умолчанию, отличными от NULL.

tp_new передаётся тип, экземпляр которого создаётся (не обязательно CustomType, если создаётся экземпляр подкласса), и любые аргументы, переданные при вызове типа; ожидается, что он вернёт созданный экземпляр. Обработчики tp_new всегда принимают позиционные и ключевые аргументы, но часто игнорируют их, оставляя обработку аргументов методам-инициализаторам (также известным как tp_init в C или __init__ в Python).

Примечание

tp_new не должен вызывать tp_init явно, так как интерпретатор сделает это сам.

Реализация tp_new вызывает слот tp_alloc для выделения памяти:

self = (CustomObject *) type->tp_alloc(type, 0);

Поскольку выделение памяти может завершиться неудачей, мы должны проверить результат tp_alloc на NULL перед продолжением.

Примечание

Мы не заполняли слот tp_alloc самостоятельно. Вместо этого PyType_Ready() заполняет его для нас, наследуя его от базового класса, которым по умолчанию является object. Большинство типов используют стратегию выделения по умолчанию.

Примечание

При создании кооперативного tp_new (такого, который вызывает tp_new или __new__() базового типа) нельзя не пытаться определить, какой метод вызывать, используя порядок разрешения методов во время выполнения. Всегда статически определяйте тип, который будете вызывать, и вызывайте его tp_new напрямую или через type->tp_base->tp_new. Если этого не делать, подклассы Python вашего типа, которые также наследуются от других классов, определённых в Python, могут работать некорректно. (В частности, вы не сможете создать экземпляры таких подклассов без получения TypeError.)

Мы также определяем функцию инициализации, которая принимает аргументы для предоставления начальных значений экземпляру:

static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL, *tmp;

    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.

.tp_init = (initproc) Custom_init,

Слот tp_init открыт в Python как метод __init__(). Он используется для инициализации объекта после его создания. Инициализаторы всегда принимают позиционные и именованные аргументы и должны возвращать либо 0 в случае успеха, либо -1 в случае ошибки.

В отличие от обработчика tp_new, нет никакой гарантии, что tp_init будет вызван (например, модуль pickle по умолчанию не вызывает __init__() для распакованных экземпляров). Он также может быть вызван несколько раз. Кто угодно может вызвать метод __init__() на наших объектах. По этой причине нужно быть особенно осторожным при присвоении новых значений атрибутов. Может возникнуть соблазн, например, присвоить члену first такое значение:

if (first) {
    Py_XDECREF(self->first);
    Py_INCREF(first);
    self->first = first;
}

But this would be risky. Our type doesn’t restrict the type of the first member, so it could be any kind of object. It could have a destructor that causes code to be executed that tries to access the first member; or that destructor could release the Global interpreter Lock and let arbitrary code run in other threads that accesses and modifies our object.

Чтобы быть параноидальными и защитить себя от такой возможности, мы почти всегда переназначаем члены перед уменьшением их счётчиков ссылок. Когда этого делать не нужно?

  • когда мы точно знаем, что счётчик ссылок больше 1;

  • when we know that deallocation of the object [1] will neither release the GIL nor cause any calls back into our type’s code;

  • когда уменьшается счётчик ссылок в обработчике tp_dealloc для типа, который не поддерживает циклическую сборку мусора [2].

Мы хотим предоставить доступ к переменным экземпляра как к атрибутам. Есть несколько способов сделать это. Самый простой – определить определения членов:

static PyMemberDef Custom_members[] = {
    {"first", Py_T_OBJECT_EX, offsetof(CustomObject, first), 0,
     "first name"},
    {"last", Py_T_OBJECT_EX, offsetof(CustomObject, last), 0,
     "last name"},
    {"number", Py_T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Страж */
};

и поместить определения в слот tp_members:

.tp_members = Custom_members,

Каждое определение члена содержит имя члена, тип, смещение, флаги доступа и строку документации. Подробнее см. раздел Общее управление атрибутами ниже.

Недостаток этого подхода в том, что он не даёт способа ограничить типы объектов, которые можно присваивать атрибутам Python. Мы ожидаем, что имя и фамилия будут строками, но можно присвоить любые объекты Python. Кроме того, атрибуты можно удалить, установив указатели C в NULL. Даже если мы можем гарантировать, что члены инициализируются значениями, отличными от NULL, члены могут быть установлены в NULL, если атрибуты удалены.

Мы определяем один метод Custom.name(), который выводит имя объекта как конкатенацию имени и фамилии.

static PyObject *
Custom_name(CustomObject *self, PyObject *Py_UNUSED(ignored))
{
    if (self->first == NULL) {
        PyErr_SetString(PyExc_AttributeError, "first");
        return NULL;
    }
    if (self->last == NULL) {
        PyErr_SetString(PyExc_AttributeError, "last");
        return NULL;
    }
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}

Метод реализован как функция на C, которая принимает экземпляр Custom (или подкласс Custom) в качестве первого аргумента. Методы всегда принимают экземпляр в качестве первого аргумента. Методы часто также принимают позиционные и именованные аргументы, но в данном случае мы не принимаем никаких и не нуждаемся в кортеже позиционных аргументов или словаре именованных аргументов. Этот метод эквивалентен следующему методу Python:

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

Обратите внимание, что нужно проверять возможность того, что наши члены first и last равны NULL. Это потому, что их можно удалить, и в этом случае они устанавливаются в NULL. Было бы лучше запретить удаление этих атрибутов и ограничить значения атрибутов строками. Как это сделать, мы увидим в следующем разделе.

Теперь, когда мы определили метод, нужно создать массив определений методов:

static PyMethodDef Custom_methods[] = {
    {"name", (PyCFunction) Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* Страж */
};

(обратите внимание, что мы использовали флаг METH_NOARGS, чтобы указать, что метод не ожидает аргументов, кроме self)

и назначить его слоту tp_methods:

.tp_methods = Custom_methods,

Наконец, сделаем наш тип пригодным для использования в качестве базового класса для создания подклассов. Мы написали наши методы достаточно осторожно, чтобы они не делали никаких предположений о типе создаваемого или используемого объекта, поэтому всё, что нам нужно сделать, – это добавить Py_TPFLAGS_BASETYPE в определение флага нашего класса:

.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,

Мы переименовываем PyInit_custom() в PyInit_custom2(), обновляем имя модуля в структуре PyModuleDef и обновляем полное имя класса в структуре PyTypeObject.

Наконец, обновляем файл setup.py, чтобы включить новый модуль,

from setuptools import Extension, setup
setup(ext_modules=[
    Extension("custom", ["custom.c"]),
    Extension("custom2", ["custom2.c"]),
])

а затем переустанавливаем, чтобы можно было import custom2:

$ python -m pip install .

2.3. Предоставление более точного управления атрибутами данныхProviding finer control over data attributes

В этом разделе мы предоставим более тонкий контроль над тем, как устанавливаются атрибуты first и last в примере Custom. В предыдущей версии нашего модуля переменные экземпляра first и last могли быть установлены в нестроковые значения или даже удалены. Мы хотим гарантировать, что эти атрибуты всегда содержат строки.

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stddef.h> /* для offsetof() */

typedef struct {
    PyObject_HEAD
    PyObject *first; /* имя */
    PyObject *last;  /* фамилия */
    int number;
} CustomObject;

static void
Custom_dealloc(CustomObject *self)
{
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free((PyObject *) self);
}

static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = PyUnicode_FromString("");
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = PyUnicode_FromString("");
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|UUi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;

    if (first) {
        Py_SETREF(self->first, Py_NewRef(first));
    }
    if (last) {
        Py_SETREF(self->last, Py_NewRef(last));
    }
    return 0;
}

static PyMemberDef Custom_members[] = {
    {"number", Py_T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Страж */
};

static PyObject *
Custom_getfirst(CustomObject *self, void *closure)
{
    return Py_NewRef(self->first);
}

static int
Custom_setfirst(CustomObject *self, PyObject *value, void *closure)
{
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the first attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The first attribute value must be a string");
        return -1;
    }
    Py_SETREF(self->first, Py_NewRef(value));
    return 0;
}

static PyObject *
Custom_getlast(CustomObject *self, void *closure)
{
    return Py_NewRef(self->last);
}

static int
Custom_setlast(CustomObject *self, PyObject *value, void *closure)
{
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the last attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The last attribute value must be a string");
        return -1;
    }
    Py_SETREF(self->last, Py_NewRef(value));
    return 0;
}

static PyGetSetDef Custom_getsetters[] = {
    {"first", (getter) Custom_getfirst, (setter) Custom_setfirst,
     "first name", NULL},
    {"last", (getter) Custom_getlast, (setter) Custom_setlast,
     "last name", NULL},
    {NULL}  /* Страж */
};

static PyObject *
Custom_name(CustomObject *self, PyObject *Py_UNUSED(ignored))
{
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}

static PyMethodDef Custom_methods[] = {
    {"name", (PyCFunction) Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* Страж */
};

static PyTypeObject CustomType = {
    .ob_base = PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom3.Custom",
    .tp_doc = PyDoc_STR("Custom objects"),
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_new = Custom_new,
    .tp_init = (initproc) Custom_init,
    .tp_dealloc = (destructor) Custom_dealloc,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
    .tp_getset = Custom_getsetters,
};

static PyModuleDef custommodule = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "custom3",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};

PyMODINIT_FUNC
PyInit_custom3(void)
{
    PyObject *m;
    if (PyType_Ready(&CustomType) < 0)
        return NULL;

    m = PyModule_Create(&custommodule);
    if (m == NULL)
        return NULL;

    if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
        Py_DECREF(m);
        return NULL;
    }

    return m;
}

Чтобы обеспечить больший контроль над атрибутами first и last, мы будем использовать пользовательские функции получения и установки. Вот функции для получения и установки атрибута first:

static PyObject *
Custom_getfirst(CustomObject *self, void *closure)
{
    Py_INCREF(self->first);
    return self->first;
}

static int
Custom_setfirst(CustomObject *self, PyObject *value, void *closure)
{
    PyObject *tmp;
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the first attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The first attribute value must be a string");
        return -1;
    }
    tmp = self->first;
    Py_INCREF(value);
    self->first = value;
    Py_DECREF(tmp);
    return 0;
}

Функция получения принимает объект Custom и «замыкание», которое является указателем void. В данном случае замыкание игнорируется. (Замыкание поддерживает расширенное использование, при котором данные определения передаются функциям получения и установки. Это может быть использовано, например, для создания единого набора функций получения и установки, которые решают, какой атрибут получить или установить на основе данных в замыкании.)

Функция установки принимает объект Custom, новое значение и замыкание. Новое значение может быть NULL, в этом случае атрибут удаляется. В нашей функции установки мы вызываем ошибку, если атрибут удаляется или его новое значение не является строкой.

Мы создаём массив структур PyGetSetDef:

static PyGetSetDef Custom_getsetters[] = {
    {"first", (getter) Custom_getfirst, (setter) Custom_setfirst,
     "first name", NULL},
    {"last", (getter) Custom_getlast, (setter) Custom_setlast,
     "last name", NULL},
    {NULL}  /* Страж */
};

и регистрируем его в слоте tp_getset:

.tp_getset = Custom_getsetters,

Последний элемент структуры PyGetSetDef – это «замыкание», упомянутое выше. В данном случае мы не используем замыкание, поэтому просто передаём NULL.

Также удаляем определения членов для этих атрибутов:

static PyMemberDef Custom_members[] = {
    {"number", Py_T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Страж */
};

Также нужно обновить обработчик tp_init, чтобы разрешить передачу только строк [3]:

static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL, *tmp;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|UUi", 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;
}

Благодаря этим изменениям можно гарантировать, что члены first и last никогда не будут NULL, поэтому проверки на значения NULL можно убрать почти во всех случаях. Это означает, что большинство вызовов Py_XDECREF() можно преобразовать в вызовы Py_DECREF(). Единственное место, где эти вызовы нельзя изменить, – реализация tp_dealloc, где возможна ситуация, что инициализация этих членов завершилась неудачей в tp_new.

Также переименовываем функцию инициализации модуля и имя модуля в функции инициализации, как делали раньше, и добавляем дополнительное определение в файл setup.py.

2.4. Поддержка циклической сборки мусораSupporting cyclic garbage collection

Python имеет циклический сборщик мусора (GC), который может определять ненужные объекты даже при ненулевом счётчике ссылок. Это возможно, когда объекты образуют циклы. Например, рассмотрим:

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

В этом примере создаётся список, содержащий сам себя. При его удалении у него всё ещё остаётся ссылка на самого себя. Счётчик ссылок не падает до нуля. К счастью, циклический сборщик мусора Python в конечном итоге определит, что список является мусором, и освободит его.

Во второй версии примера Custom мы разрешили хранить объекты любого типа в атрибутах first или last [4]. Кроме того, во второй и третьей версиях мы разрешили наследование от Custom, а подклассы могут добавлять произвольные атрибуты. По любой из этих двух причин объекты Custom могут участвовать в циклах:

>>> import custom3
>>> class Derived(custom3.Custom): pass
...
>>> n = Derived()
>>> n.some_attribute = n

Чтобы экземпляр Custom, участвующий в цикле ссылок, был корректно обнаружен и собран циклическим GC, наш тип Custom должен заполнить два дополнительных слота и включить флаг, активирующий эти слоты:

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stddef.h> /* для offsetof() */

typedef struct {
    PyObject_HEAD
    PyObject *first; /* имя */
    PyObject *last;  /* фамилия */
    int number;
} CustomObject;

static int
Custom_traverse(CustomObject *self, visitproc visit, void *arg)
{
    Py_VISIT(self->first);
    Py_VISIT(self->last);
    return 0;
}

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

static void
Custom_dealloc(CustomObject *self)
{
    PyObject_GC_UnTrack(self);
    Custom_clear(self);
    Py_TYPE(self)->tp_free((PyObject *) self);
}

static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = PyUnicode_FromString("");
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = PyUnicode_FromString("");
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|UUi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;

    if (first) {
        Py_SETREF(self->first, Py_NewRef(first));
    }
    if (last) {
        Py_SETREF(self->last, Py_NewRef(last));
    }
    return 0;
}

static PyMemberDef Custom_members[] = {
    {"number", Py_T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Страж */
};

static PyObject *
Custom_getfirst(CustomObject *self, void *closure)
{
    return Py_NewRef(self->first);
}

static int
Custom_setfirst(CustomObject *self, PyObject *value, void *closure)
{
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the first attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The first attribute value must be a string");
        return -1;
    }
    Py_XSETREF(self->first, Py_NewRef(value));
    return 0;
}

static PyObject *
Custom_getlast(CustomObject *self, void *closure)
{
    return Py_NewRef(self->last);
}

static int
Custom_setlast(CustomObject *self, PyObject *value, void *closure)
{
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the last attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The last attribute value must be a string");
        return -1;
    }
    Py_XSETREF(self->last, Py_NewRef(value));
    return 0;
}

static PyGetSetDef Custom_getsetters[] = {
    {"first", (getter) Custom_getfirst, (setter) Custom_setfirst,
     "first name", NULL},
    {"last", (getter) Custom_getlast, (setter) Custom_setlast,
     "last name", NULL},
    {NULL}  /* Страж */
};

static PyObject *
Custom_name(CustomObject *self, PyObject *Py_UNUSED(ignored))
{
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}

static PyMethodDef Custom_methods[] = {
    {"name", (PyCFunction) Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* Страж */
};

static PyTypeObject CustomType = {
    .ob_base = PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom4.Custom",
    .tp_doc = PyDoc_STR("Custom objects"),
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
    .tp_new = Custom_new,
    .tp_init = (initproc) Custom_init,
    .tp_dealloc = (destructor) Custom_dealloc,
    .tp_traverse = (traverseproc) Custom_traverse,
    .tp_clear = (inquiry) Custom_clear,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
    .tp_getset = Custom_getsetters,
};

static PyModuleDef custommodule = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "custom4",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};

PyMODINIT_FUNC
PyInit_custom4(void)
{
    PyObject *m;
    if (PyType_Ready(&CustomType) < 0)
        return NULL;

    m = PyModule_Create(&custommodule);
    if (m == NULL)
        return NULL;

    if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
        Py_DECREF(m);
        return NULL;
    }

    return m;
}

Во-первых, метод обхода (traversal) сообщает циклическому GC о подобъектах, которые могут участвовать в циклах:

static int
Custom_traverse(CustomObject *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 предоставляет макрос Py_VISIT(), автоматизирующий вызов функций обхода. С помощью Py_VISIT() можно минимизировать количество шаблонного кода в Custom_traverse:

static int
Custom_traverse(CustomObject *self, visitproc visit, void *arg)
{
    Py_VISIT(self->first);
    Py_VISIT(self->last);
    return 0;
}

Примечание

Реализация tp_traverse должна именовать свои аргументы именно как visit и arg, чтобы использовать Py_VISIT().

Во-вторых, нужно предоставить метод для очистки любых подобъектов, которые могут участвовать в циклах:

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

Обратите внимание на использование макроса Py_CLEAR(). Это рекомендуемый и безопасный способ очистки атрибутов данных произвольных типов с одновременным уменьшением их счётчиков ссылок. Если вместо этого вызвать Py_XDECREF() для атрибута перед установкой его в NULL, существует вероятность, что деструктор атрибута повторно вызовет код, читающий этот атрибут (особенно при наличии циклической ссылки).

Примечание

Можно эмулировать Py_CLEAR() следующим образом:

PyObject *tmp;
tmp = self->first;
self->first = NULL;
Py_XDECREF(tmp);

Тем не менее, гораздо проще и менее чревато ошибками всегда использовать Py_CLEAR() при удалении атрибута. Не пытайтесь микрооптимизировать в ущерб надёжности!

Деаллокатор Custom_dealloc может вызывать произвольный код при очистке атрибутов. Это означает, что циклический GC может быть запущен внутри функции. Поскольку GC предполагает, что счётчик ссылок не равен нулю, нужно отменить отслеживание объекта GC с помощью вызова PyObject_GC_UnTrack() перед очисткой членов. Вот наша переработанная версия деаллокатора с использованием PyObject_GC_UnTrack() и Custom_clear:

static void
Custom_dealloc(CustomObject *self)
{
    PyObject_GC_UnTrack(self);
    Custom_clear(self);
    Py_TYPE(self)->tp_free((PyObject *) self);
}

Наконец, добавляем флаг Py_TPFLAGS_HAVE_GC в флаги класса:

.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,

Вот, в общем-то, и всё. Если бы мы написали собственные обработчики tp_alloc или tp_free, их пришлось бы доработать для поддержки циклической сборки мусора. Большинство расширений используют автоматически предоставляемые версии.

2.5. Наследование от других типовSubclassing other types

Можно создавать новые типы расширений, производные от существующих типов. Проще всего наследовать от встроенных типов, поскольку расширение может легко использовать нужный ему PyTypeObject. Совместное использование структур PyTypeObject между модулями расширений может быть затруднительным.

В этом примере мы создадим тип SubList, наследующий от встроенного типа list. Новый тип будет полностью совместим с обычными списками, но будет иметь дополнительный метод increment(), увеличивающий внутренний счётчик:

>>> import sublist
>>> s = sublist.SubList(range(3))
>>> s.extend(s)
>>> print(len(s))
6
>>> print(s.increment())
1
>>> print(s.increment())
2
#define PY_SSIZE_T_CLEAN
#include <Python.h>

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

static PyObject *
SubList_increment(SubListObject *self, PyObject *unused)
{
    self->state++;
    return PyLong_FromLong(self->state);
}

static PyMethodDef SubList_methods[] = {
    {"increment", (PyCFunction) SubList_increment, METH_NOARGS,
     PyDoc_STR("increment state counter")},
    {NULL},
};

static int
SubList_init(SubListObject *self, PyObject *args, PyObject *kwds)
{
    if (PyList_Type.tp_init((PyObject *) self, args, kwds) < 0)
        return -1;
    self->state = 0;
    return 0;
}

static PyTypeObject SubListType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "sublist.SubList",
    .tp_doc = PyDoc_STR("SubList objects"),
    .tp_basicsize = sizeof(SubListObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_init = (initproc) SubList_init,
    .tp_methods = SubList_methods,
};

static PyModuleDef sublistmodule = {
    PyModuleDef_HEAD_INIT,
    .m_name = "sublist",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};

PyMODINIT_FUNC
PyInit_sublist(void)
{
    PyObject *m;
    SubListType.tp_base = &PyList_Type;
    if (PyType_Ready(&SubListType) < 0)
        return NULL;

    m = PyModule_Create(&sublistmodule);
    if (m == NULL)
        return NULL;

    Py_INCREF(&SubListType);
    if (PyModule_AddObject(m, "SubList", (PyObject *) &SubListType) < 0) {
        Py_DECREF(&SubListType);
        Py_DECREF(m);
        return NULL;
    }

    return m;
}

Как видите, исходный код очень похож на примеры Custom из предыдущих разделов. Разберём основные различия между ними.

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

Основное отличие для объектов производных типов в том, что структура объекта базового типа должна быть первым значением. Базовый тип уже включает PyObject_HEAD() в начале своей структуры.

Когда объект Python является экземпляром SubList, его указатель PyObject * можно безопасно приводить как к PyListObject *, так и к SubListObject *:

static int
SubList_init(SubListObject *self, PyObject *args, PyObject *kwds)
{
    if (PyList_Type.tp_init((PyObject *) self, args, kwds) < 0)
        return -1;
    self->state = 0;
    return 0;
}

Выше показано, как вызывать метод __init__() базового типа.

Этот шаблон важен при написании типа с пользовательскими членами tp_new и tp_dealloc. Обработчик tp_new не должен фактически выделять память для объекта с помощью своего tp_alloc, а должен позволить базовому классу обработать это, вызвав его собственный tp_new.

Структура PyTypeObject поддерживает tp_base указывающее конкретный базовый класс типа. Из-за проблем с кроссплатформенными компиляторами нельзя заполнить это поле напрямую ссылкой на PyList_Type; это следует сделать позже, в функции инициализации модуля:

PyMODINIT_FUNC
PyInit_sublist(void)
{
    PyObject* m;
    SubListType.tp_base = &PyList_Type;
    if (PyType_Ready(&SubListType) < 0)
        return NULL;

    m = PyModule_Create(&sublistmodule);
    if (m == NULL)
        return NULL;

    Py_INCREF(&SubListType);
    if (PyModule_AddObject(m, "SubList", (PyObject *) &SubListType) < 0) {
        Py_DECREF(&SubListType);
        Py_DECREF(m);
        return NULL;
    }

    return m;
}

Перед вызовом PyType_Ready() структура типа должна иметь заполненный слот tp_base. При наследовании от существующего типа необязательно заполнять слот tp_alloc значением PyType_GenericNew() – функция выделения памяти из базового типа будет унаследована.

После этого вызов PyType_Ready() и добавление объекта типа в модуль выполняется так же, как в базовых примерах Custom.

Сноски