2. Определение типов расширений: Учебник

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

2.1. Основы

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

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

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

Примечание

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

#define PY_SSIZE_T_CLEAN
#include <Python.h>

typedef struct {
    PyObject_HEAD
    /* Type-specific fields go here. */
} 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;

    if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
        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 соответственно). Макрос нужен для того, чтобы абстрагироваться от разметки и включить дополнительные поля в debug builds.

Примечание

После макроса 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 имеет гораздо больше fields, чем определение, приведенное выше. Оставшиеся поля будут заполнены нулями компилятором языка Си, и обычно принято не указывать их явно, если они вам не нужны.

Мы разберем его на части, по одному полю за раз:

.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 в соответствующие флаги.

Мы предоставляем строку doc для типа в 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.

if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
    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. Добавление данных и методов в пример Basic

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

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stddef.h> /* for offsetof() */

typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    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}  /* Sentinel */
};

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

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; /* first name */
    PyObject *last;  /* last name */
    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;
}

Но это было бы рискованно. Наш тип не ограничивает тип члена first, так что это может быть любой объект. У него может быть деструктор, который вызовет выполнение кода, пытающегося получить доступ к члену first; или этот деструктор может освободить член Global interpreter Lock и позволить произвольному коду выполняться в других потоках, которые получают доступ к нашему объекту и изменяют его.

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

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

  • когда мы знаем, что деаллокация объекта [1] не освободит GIL и не вызовет никаких обратных вызовов в коде нашего типа;

  • при уменьшении количества ссылок в обработчике 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}  /* Sentinel */
};

и поместите определения в слот 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}  /* Sentinel */
};

(обратите внимание, что мы использовали флаг 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. Обеспечение более тонкого контроля над атрибутами данных

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

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stddef.h> /* for offsetof() */

typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    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}  /* Sentinel */
};

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

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

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, мы будем использовать пользовательские функции getter и setter. Вот функции для получения и установки атрибута 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;
}

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

Функции setter передается объект 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}  /* Sentinel */
};

и зарегистрировать его в слоте tp_getset:

.tp_getset = Custom_getsetters,

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

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

static PyMemberDef Custom_members[] = {
    {"number", Py_T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};

Нам также нужно обновить обработчик 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. Поддержка циклической сборки мусора

В Python есть функция cyclic garbage collector (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> /* for offsetof() */

typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    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}  /* Sentinel */
};

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

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

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

Во-первых, метод обхода позволяет циклическому 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 в класс flags:

.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,

Это практически все. Если бы мы написали собственные обработчики tp_alloc или tp_free, нам пришлось бы модифицировать их для циклической сборки мусора. Большинство расширений будут использовать автоматически предоставляемые версии.

2.5. Подклассификация других типов

Можно создавать новые типы расширений, которые являются производными от существующих типов. Проще всего наследовать от встроенных типов, так как расширение может легко использовать необходимые ему 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;

    if (PyModule_AddObjectRef(m, "SubList", (PyObject *) &SubListType) < 0) {
        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;

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

    return m;
}

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

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

Сноски