Определение типов расширения: учебное руководство
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;
}
Сразу приходится усвоить немало информации, но, надеемся, некоторые элементы покажутся знакомыми по предыдущей главе. Этот файл определяет три вещи:
- Содержимое объекта
Custom: это структураCustomObject, которая выделяется один раз для каждого экземпляраCustom. - Поведение типа
Custom: это структураCustomType, определяющая набор флагов и указателей на функции, которые интерпретатор проверяет при запросе определённых операций. - Способ инициализации модуля
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__() для восстановленных из 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 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, участвующий в цикле ссылок, мог быть корректно обнаружен и собран циклическим сборщиком мусора, наш тип 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;
}
Сначала метод обхода сообщает циклическому сборщику мусора о подобъектах, которые могут участвовать в циклах:
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 = {
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.
Сноски
© 2001–2024 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.12/extending/newtypes_tutorial.html