Spec-Zone.ru › Python 3.12

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

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 = {
    .ob_base = PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom.Custom",
    .tp_doc = PyDoc_STR("Custom objects"),
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_new = PyType_GenericNew,
};

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

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

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

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

    return m;
}

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

  1. Что содержит объект Custom: это структура CustomObject, которая выделяется один раз для каждого экземпляра Custom.
  2. Как ведет себя тип Custom: это структура CustomType, которая определяет набор флагов и указателей на функции, которые интерпретатор проверяет, когда запрашиваются определенные операции.
  3. Как инициализировать модуль custom: это функция PyInit_custom и связанная структура custommodule.

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

typedef struct {
    PyObject_HEAD
} CustomObject;

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

Примечание

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

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

typedef struct {
    PyObject_HEAD
    double ob_fval;
} PyFloatObject;

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

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

Примечание

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

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

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

.ob_base = PyVarObject_HEAD_INIT(NULL, 0)

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

.tp_name = "custom.Custom",

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

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

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

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

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

Примечание

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

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

.tp_flags = Py_TPFLAGS_DEFAULT,

Все типы должны включать эту константу в своих флагах. Это включает все члены, определенные по крайней мере до Python 3.3. Если вам нужны дополнительные члены, вам нужно будет выполнить операцию ИЛИ соответствующих флагов.

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

.tp_doc = PyDoc_STR("Custom objects"),

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

.tp_new = PyType_GenericNew,

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

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

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

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

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

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

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

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

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

в файл под названием pyproject.toml, и

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

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

$ python -m pip install .

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

Не так уж и сложно, правда?

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

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

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

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

typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    int number;
} CustomObject;

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

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

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

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

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

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

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

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

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

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

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

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

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

    return m;
}

В этой версии модуля есть ряд изменений.

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

Структура объекта обновлена соответственно:

typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    int number;
} CustomObject;

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

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

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

.tp_dealloc = (destructor) Custom_dealloc,

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

Примечание

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

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

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

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

.tp_new = Custom_new,

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

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

Примечание

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

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

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

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

Примечание

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

Примечание

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

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

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

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

    if (first) {
        tmp = self->first;
        Py_INCREF(first);
        self->first = first;
        Py_XDECREF(tmp);
    }
    if (last) {
        tmp = self->last;
        Py_INCREF(last);
        self->last = last;
        Py_XDECREF(tmp);
    }
    return 0;
}

путем заполнения слота tp_init.

.tp_init = (initproc) Custom_init,

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

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

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

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

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

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

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

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

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

.tp_members = Custom_members,

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

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

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

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

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

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

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

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

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

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

и присвойте его слоту tp_methods:

.tp_methods = Custom_methods,

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

.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,

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

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

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

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

$ python -m pip install .

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

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

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

typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    int number;
} CustomObject;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return m;
}

Для обеспечения более точного управления атрибутами first и last мы будем использовать пользовательские функции-геттеры и сеттеры. Вот функции для получения и установки атрибута 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", Py_T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};

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

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

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

    if (first) {
        tmp = self->first;
        Py_INCREF(first);
        self->first = first;
        Py_DECREF(tmp);
    }
    if (last) {
        tmp = self->last;
        Py_INCREF(last);
        self->last = last;
        Py_DECREF(tmp);
    }
    return 0;
}

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

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

2.4. Поддержка циклического сбора мусора

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

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

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

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

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

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

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

typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    int number;
} CustomObject;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return m;
}

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

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

Для каждого объекта-потомка, который может участвовать в циклах, необходимо вызвать функцию visit(), которая передаётся методу обхода. Функция visit() принимает в качестве аргументов подобъект и дополнительный аргумент arg, переданный методу обхода. Она возвращает целочисленное значение, которое должно быть возвращено, если оно не равно нулю.

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

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

Примечание

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

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

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

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

Примечание

Вы можете эмулировать Py_CLEAR(), написав:

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

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

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

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

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

.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.12/extending/newtypes_tutorial.html

Spec-Zone.ru

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