Spec-Zone.ru › Python 3.11

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

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

2.1. Основы

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

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

Лучше всего объяснить это на примере, поэтому приведем минимальный, но полноценный модуль, который определяет новый тип с именем Custom внутри модуля расширения на C custom:

Примечание

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

#define PY_SSIZE_T_CLEAN
#include <Python.h>

typedef struct {
    PyObject_HEAD
    /* 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, которая выделяется один раз для каждого экземпляра Custom.
  2. Поведение типа Custom: это структура CustomType, в которой определен набор флагов и указателей на функции, проверяемых интерпретатором при выполнении определенных операций.
  3. Способ инициализации модуля custom: это функция PyInit_custom и связанная с ней структура custommodule.

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

typedef struct {
    PyObject_HEAD
} CustomObject;

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

Примечание

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

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

typedef struct {
    PyObject_HEAD
    double ob_fval;
} PyFloatObject;

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

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

Примечание

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

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

Давайте разберем его по частям, поле за полем:

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 = 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

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

Оказалось не так уж сложно, правда?

Разумеется, нынешний тип Custom довольно непримечателен. У него нет данных, и он ничего не делает. Более того, для него нельзя создавать подклассы.

Примечание

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

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

Давайте расширим базовый пример, добавив данные и методы. Также сделаем тип пригодным для использования в качестве базового класса. Создадим новый модуль, 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: 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; либо этот деструктор может освободить глобальную блокировку интерпретатора и позволить произвольному коду в других потоках выполняться с доступом к нашему объекту и изменять его.

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

  • когда мы точно знаем, что счётчик ссылок больше 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);
}

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

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

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

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

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

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

static PyGetSetDef Custom_getsetters[] = {
    {"first", (getter) Custom_getfirst, (setter) Custom_setfirst,
     "first name", NULL},
    {"last", (getter) Custom_getlast, (setter) Custom_setlast,
     "last name", NULL},
    {NULL}  /* 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, участвующий в цикле ссылок, можно было корректно обнаружить и собрать циклическим сборщиком мусора, тип 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;
}

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

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

Примечание

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

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

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 может вызывать произвольный код при очистке атрибутов. Это означает, что циклический сборщик мусора может быть запущен внутри функции. Поскольку сборщик мусора предполагает, что счётчик ссылок не равен нулю, перед очисткой членов нужно исключить объект из отслеживания сборщиком, вызвав 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 = 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]

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

[2]

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

[3]

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

[4]

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

© 2001–2024 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.11/extending/newtypes_tutorial.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API