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

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

2.1. Основы

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

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

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

Примечание

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

#define PY_SSIZE_T_CLEAN
#include <Python.h>

typedef struct {
    PyObject_HEAD
    /* Здесь находятся поля, относящиеся к конкретному типу. */
} CustomObject;

static PyTypeObject CustomType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom.Custom",
    .tp_doc = "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, которая присваивается один раз для каждой Custom сущности.
  2. Теперь поведение Custom type: это структура CustomType, которая определяет набор флагов и указателей функций, проверяемых интерпретатору при запросе определенных операций.
  3. Как инициализировать модуль custom: это функция PyInit_custom и связанная структура custommodule.

Первый бит -:

typedef struct {
    PyObject_HEAD
} CustomObject;

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

Примечание

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

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

typedef struct {
    PyObject_HEAD
    double ob_fval;
} PyFloatObject;

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

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

Примечание

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

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

Мы разберем его по одному полю за раз:

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

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

.tp_doc = "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

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

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

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

Примечание

Несмотря на то, что в этой документации представлен стандартный модуль distutils для создания расширений C, в реальных сценариях использования рекомендуется использовать более новую и лучше поддерживаемую библиотеку setuptools. Документация о том, как это сделать, не является областью рассмотрения для этого документа и может быть найдена в Руководство пользователя пакетизации Python.

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

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

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

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, *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 = "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, 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__() на unpickled сущности). Его также можно вызвать несколько раз. Любой может вызвать метод __init__() на наших объектах. Поэтому при присвоении нового атрибута значения необходимо проявлять особую осторожность. У нас может возникнуть искушение, например, назначить член first вот так:

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

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

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

  • когда мы абсолютно уверены, что счетчик ссылок больше 1;
  • когда мы знаем, что деалокация объекта [1] не приведет ни к освобождению GIL, ни к каким-либо вызовам в коде; нашего типа;
  • при уменьшении числа ссылок в tp_dealloc обработчик для type, который не поддерживает циклическую сборку мусора [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);
}

Метод реализуется как функция 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 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; /* имя */
    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, *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}  /* Страж */
};

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}  /* Страж */
};

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 = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom3.Custom",
    .tp_doc = "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;
}

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

Функция сеттера передается объекту 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 содержит циклический сборщик мусора (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; /* имя */
    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, *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}  /* Страж */
};

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}  /* Страж */
};

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 = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom4.Custom",
    .tp_doc = "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 к флагам класса:

.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 = "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 подклассы и, следовательно, все еще создавать счетчиков ссылок.