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

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

2.1. Основы

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

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

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

Примечание

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

#define PY_SSIZE_T_CLEAN
#include <Python.h>

typedef struct {
    PyObject_HEAD
    /* Type-specific fields go here. */
} CustomObject;

static PyTypeObject CustomType = {
    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 = {
    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 struct, который выделяется один раз для каждого экземпляра Custom.

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

  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; например, вот определение для стандартных floats в Python:

typedef struct {
    PyObject_HEAD
    double ob_fval;
} PyFloatObject;

Второй бит - это определение объекта типа.

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

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

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. Если вам нужны дополнительные члены, вам нужно будет добавить соответствующие флаги.

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

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 и:

from distutils.core import setup, Extension
setup(name="custom", version="1.0",
      ext_modules=[Extension("custom", ["custom.c"])])

в файле под названием setup.py; затем набираем

$ python setup.py build

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

Это было не так сложно, не так ли?

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

Примечание

Хотя в этой документации представлен стандартный модуль distutils для создания расширений на языке Си, в реальных случаях рекомендуется использовать более новую и лучше поддерживаемую библиотеку setuptools. Документация о том, как это сделать, выходит за рамки данного документа и может быть найдена в Python Packaging User’s Guide.

2.2. Добавление данных и методов в пример Basic

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

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include "structmember.h"

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

static PyMemberDef Custom_members[] = {
    {"first", T_OBJECT_EX, offsetof(CustomObject, first), 0,
     "first name"},
    {"last", T_OBJECT_EX, offsetof(CustomObject, last), 0,
     "last name"},
    {"number", 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 = {
    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 = {
    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;

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

    return m;
}

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

Мы добавили дополнительную функцию:

#include <structmember.h>

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

Тип Custom теперь имеет три атрибута данных в своей C struct, 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", T_OBJECT_EX, offsetof(CustomObject, first), 0,
     "first name"},
    {"last", T_OBJECT_EX, offsetof(CustomObject, last), 0,
     "last name"},
    {"number", 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);
}

Метод реализован как функция языка Си, принимающая в качестве первого аргумента экземпляр 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 struct и обновляем полное имя класса в PyTypeObject struct.

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

from distutils.core import setup, Extension
setup(name="custom", version="1.0",
      ext_modules=[
         Extension("custom", ["custom.c"]),
         Extension("custom2", ["custom2.c"]),
         ])

2.3. Обеспечение более тонкого контроля над атрибутами данных

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

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include "structmember.h"

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

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

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

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

static int
Custom_setlast(CustomObject *self, PyObject *value, void *closure)
{
    PyObject *tmp;
    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;
    }
    tmp = self->last;
    Py_INCREF(value);
    self->last = value;
    Py_DECREF(tmp);
    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 = {
    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 = {
    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;

    Py_INCREF(&CustomType);
    if (PyModule_AddObject(m, "Custom", (PyObject *) &CustomType) < 0) {
        Py_DECREF(&CustomType);
        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", 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 "structmember.h"

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

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

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)
{
    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_INCREF(value);
    Py_CLEAR(self->first);
    self->first = value;
    return 0;
}

static PyObject *
Custom_getlast(CustomObject *self, void *closure)
{
    Py_INCREF(self->last);
    return 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_INCREF(value);
    Py_CLEAR(self->last);
    self->last = 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 = {
    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 = {
    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;

    Py_INCREF(&CustomType);
    if (PyModule_AddObject(m, "Custom", (PyObject *) &CustomType) < 0) {
        Py_DECREF(&CustomType);
        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;

    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.

Сноски

1

Это верно, когда мы знаем, что объект является базовым типом, таким как строка или float.

2

Мы полагались на это в обработчике tp_dealloc в этом примере, потому что наш тип не поддерживает сборку мусора.

3

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

4

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

Вернуться на верх