Определение типов расширений: руководство
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 int
custom_module_exec(PyObject *m)
{
if (PyType_Ready(&CustomType) < 0) {
return -1;
}
if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
return -1;
}
return 0;
}
static PyModuleDef_Slot custom_module_slots[] = {
{Py_mod_exec, custom_module_exec},
// Just use this while using static types
{Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
{0, NULL}
};
static PyModuleDef custom_module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "custom",
.m_doc = "Example module that creates an extension type.",
.m_size = 0,
.m_slots = custom_module_slots,
};
PyMODINIT_FUNC
PyInit_custom(void)
{
return PyModuleDef_Init(&custom_module);
}
Сразу приходится усвоить довольно много, но, будем надеяться, некоторые элементы покажутся знакомыми по предыдущей главе. Этот файл определяет три вещи:
- Содержимое объекта
Custom: это структураCustomObject, которая выделяется отдельно для каждого экземпляраCustom. - Поведение типа
Custom: это структураCustomType, в которой задаётся набор флагов и указателей на функции, проверяемых интерпретатором при запросе определённых операций. - Способ определения и выполнения модуля
custom: это функцияPyInit_customи связанная с ней структураcustom_moduleдля определения модуля, а также функцияcustom_module_execдля настройки нового объекта модуля.
Первая часть:
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,
Всё остальное в файле должно быть знакомо, за исключением нескольких строк кода в custom_module_exec():
if (PyType_Ready(&CustomType) < 0) {
return -1;
}
Это инициализирует тип Custom, задавая ряду его членов соответствующие значения по умолчанию, включая ob_type, которому мы изначально присвоили значение NULL.
if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
return -1;
}
Это добавляет тип в словарь модуля. Благодаря этому мы можем создавать экземпляры 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 int
custom_module_exec(PyObject *m)
{
if (PyType_Ready(&CustomType) < 0) {
return -1;
}
if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
return -1;
}
return 0;
}
static PyModuleDef_Slot custom_module_slots[] = {
{Py_mod_exec, custom_module_exec},
{Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
{0, NULL}
};
static PyModuleDef custom_module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "custom2",
.m_doc = "Example module that creates an extension type.",
.m_size = 0,
.m_slots = custom_module_slots,
};
PyMODINIT_FUNC
PyInit_custom2(void)
{
return PyModuleDef_Init(&custom_module);
}
В этой версии модуля есть ряд изменений.
Теперь тип 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__() для восстановленных из pickle экземпляров). Он также может вызываться несколько раз. Любой может вызвать метод __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", 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 int
custom_module_exec(PyObject *m)
{
if (PyType_Ready(&CustomType) < 0) {
return -1;
}
if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
return -1;
}
return 0;
}
static PyModuleDef_Slot custom_module_slots[] = {
{Py_mod_exec, custom_module_exec},
{Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
{0, NULL}
};
static PyModuleDef custom_module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "custom3",
.m_doc = "Example module that creates an extension type.",
.m_size = 0,
.m_slots = custom_module_slots,
};
PyMODINIT_FUNC
PyInit_custom3(void)
{
return PyModuleDef_Init(&custom_module);
}
Чтобы обеспечить более точное управление атрибутами 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, участвующий в цикле ссылок, можно было правильно обнаружить и собрать с помощью циклического сборщика мусора, наш тип 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 int
custom_module_exec(PyObject *m)
{
if (PyType_Ready(&CustomType) < 0) {
return -1;
}
if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
return -1;
}
return 0;
}
static PyModuleDef_Slot custom_module_slots[] = {
{Py_mod_exec, custom_module_exec},
{Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
{0, NULL}
};
static PyModuleDef custom_module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "custom4",
.m_doc = "Example module that creates an extension type.",
.m_size = 0,
.m_slots = custom_module_slots,
};
PyMODINIT_FUNC
PyInit_custom4(void)
{
return PyModuleDef_Init(&custom_module);
}
Во-первых, метод обхода сообщает циклическому сборщику мусора о подобъектах, которые могут участвовать в циклах:
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(), автоматизирующий вызов функций 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 при очистке атрибутов может вызывать произвольный код. Это означает, что циклический сборщик мусора может запускаться внутри функции. Поскольку сборщик мусора предполагает, что счётчик ссылок не равен нулю, перед очисткой членов объекта необходимо исключить его из отслеживания сборщиком, вызвав 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 = {
.ob_base = 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 int
sublist_module_exec(PyObject *m)
{
SubListType.tp_base = &PyList_Type;
if (PyType_Ready(&SubListType) < 0) {
return -1;
}
if (PyModule_AddObjectRef(m, "SubList", (PyObject *) &SubListType) < 0) {
return -1;
}
return 0;
}
static PyModuleDef_Slot sublist_module_slots[] = {
{Py_mod_exec, sublist_module_exec},
{Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
{0, NULL}
};
static PyModuleDef sublist_module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "sublist",
.m_doc = "Example module that creates an extension type.",
.m_size = 0,
.m_slots = sublist_module_slots,
};
PyMODINIT_FUNC
PyInit_sublist(void)
{
return PyModuleDef_Init(&sublist_module);
}
Как видно, исходный код очень похож на примеры 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; это нужно сделать в функции Py_mod_exec:
static int
sublist_module_exec(PyObject *m)
{
SubListType.tp_base = &PyList_Type;
if (PyType_Ready(&SubListType) < 0) {
return -1;
}
if (PyModule_AddObjectRef(m, "SubList", (PyObject *) &SubListType) < 0) {
return -1;
}
return 0;
}
Перед вызовом PyType_Ready() в структуре типа должен быть заполнен слот tp_base. При наследовании существующего типа заполнять слот tp_alloc значением PyType_GenericNew() не требуется — функция выделения памяти будет унаследована от базового типа.
После этого вызов PyType_Ready() и добавление объекта типа в модуль выполняются так же, как и в базовых примерах Custom.
Сноски
© 2001–2025 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.13/extending/newtypes_tutorial.html