Spec-Zone.ru › Python 3.14

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

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

2.1. Основы

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

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

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

Примечание

Здесь мы показываем традиционный способ определения статических типов расширения. Для большинства случаев этого будет достаточно. API C также позволяет определять типы расширения, выделяемые в куче, с помощью функции 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);
}

Сразу может показаться, что здесь слишком много информации, но, надеемся, кое-что покажется знакомым по предыдущей главе. В этом файле определены три вещи:

  1. Содержимое объекта Custom: это структура CustomObject, которая выделяется один раз для каждого экземпляра Custom.
  2. Поведение типа Custom: это структура CustomType, определяющая набор флагов и указателей на функции, которые проверяет интерпретатор при запросе определённых операций.
  3. Определение и выполнение модуля 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(PyObject *op)
{
    CustomObject *self = (CustomObject *) op;
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free(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 = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

static int
Custom_init(PyObject *op, PyObject *args, PyObject *kwds)
{
    CustomObject *self = (CustomObject *) op;
    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(PyObject *op, PyObject *Py_UNUSED(dummy))
{
    CustomObject *self = (CustomObject *) op;
    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", 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 = Custom_init,
    .tp_dealloc = 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(PyObject *op)
{
    CustomObject *self = (CustomObject *) op;
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free(self);
}

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

.tp_dealloc = Custom_dealloc,

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

Примечание

Приведение типа к CustomObject * выше необходимо, потому что мы определили Custom_dealloc с аргументом типа PyObject *, а указатель на функцию tp_dealloc ожидает аргумент типа PyObject *. Назначая функцию слоту tp_dealloc типа, мы объявляем, что её можно вызывать только для экземпляров нашего класса CustomObject, поэтому приведение к (CustomObject *) безопасно. Вот и полиморфизм в объектно-ориентированном стиле — на C!

В существующем коде или в предыдущих версиях этого руководства вы можете встретить похожие функции, которые принимают указатель непосредственно на структуру объекта подтипа (CustomObject*), например:

Custom_dealloc(CustomObject *self)
{
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free((PyObject *) self);
}
...
.tp_dealloc = (destructor) Custom_dealloc,

На всех архитектурах, поддерживаемых CPython, это работает так же, но, согласно стандарту 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(PyObject *op, PyObject *args, PyObject *kwds)
{
    CustomObject *self = (CustomObject *) op;
    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 = 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(PyObject *op, PyObject *Py_UNUSED(dummy))
{
    CustomObject *self = (CustomObject *) op;
    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", 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(PyObject *op)
{
    CustomObject *self = (CustomObject *) op;
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free(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 = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

static int
Custom_init(PyObject *op, PyObject *args, PyObject *kwds)
{
    CustomObject *self = (CustomObject *) op;
    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(PyObject *op, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    return Py_NewRef(self->first);
}

static int
Custom_setfirst(PyObject *op, PyObject *value, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    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(PyObject *op, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    return Py_NewRef(self->last);
}

static int
Custom_setlast(PyObject *op, PyObject *value, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    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", Custom_getfirst, Custom_setfirst,
     "first name", NULL},
    {"last", Custom_getlast, Custom_setlast,
     "last name", NULL},
    {NULL}  /* Sentinel */
};

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

static PyMethodDef Custom_methods[] = {
    {"name", 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 = Custom_init,
    .tp_dealloc = 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(PyObject *op, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    Py_INCREF(self->first);
    return self->first;
}

static int
Custom_setfirst(PyObject *op, PyObject *value, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    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", Custom_getfirst, Custom_setfirst,
     "first name", NULL},
    {"last", Custom_getlast, 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(PyObject *op, PyObject *args, PyObject *kwds)
{
    CustomObject *self = (CustomObject *) op;
    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(PyObject *op, visitproc visit, void *arg)
{
    CustomObject *self = (CustomObject *) op;
    Py_VISIT(self->first);
    Py_VISIT(self->last);
    return 0;
}

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

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

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

static int
Custom_init(PyObject *op, PyObject *args, PyObject *kwds)
{
    CustomObject *self = (CustomObject *) op;
    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(PyObject *op, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    return Py_NewRef(self->first);
}

static int
Custom_setfirst(PyObject *op, PyObject *value, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    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(PyObject *op, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    return Py_NewRef(self->last);
}

static int
Custom_setlast(PyObject *op, PyObject *value, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    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", Custom_getfirst, Custom_setfirst,
     "first name", NULL},
    {"last", Custom_getlast, Custom_setlast,
     "last name", NULL},
    {NULL}  /* Sentinel */
};

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

static PyMethodDef Custom_methods[] = {
    {"name", 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 = Custom_init,
    .tp_dealloc = Custom_dealloc,
    .tp_traverse = Custom_traverse,
    .tp_clear = 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);
}

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

static int
Custom_traverse(PyObject *op, visitproc visit, void *arg)
{
    CustomObject *self = (CustomObject *) op;
    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(PyObject *op, visitproc visit, void *arg)
{
    CustomObject *self = (CustomObject *) op;
    Py_VISIT(self->first);
    Py_VISIT(self->last);
    return 0;
}

Примечание

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

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

static int
Custom_clear(PyObject *op)
{
    CustomObject *self = (CustomObject *) op;
    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(PyObject *op)
{
    PyObject_GC_UnTrack(op);
    (void)Custom_clear(op);
    Py_TYPE(op)->tp_free(op);
}

Наконец, добавим флаг 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(PyObject *op, PyObject *Py_UNUSED(dummy))
{
    SubListObject *self = (SubListObject *) op;
    self->state++;
    return PyLong_FromLong(self->state);
}

static PyMethodDef SubList_methods[] = {
    {"increment", SubList_increment, METH_NOARGS,
     PyDoc_STR("increment state counter")},
    {NULL},
};

static int
SubList_init(PyObject *op, PyObject *args, PyObject *kwds)
{
    SubListObject *self = (SubListObject *) op;
    if (PyList_Type.tp_init(op, 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 = 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(PyObject *op, PyObject *args, PyObject *kwds)
{
    SubListObject *self = (SubListObject *) op;
    if (PyList_Type.tp_init(op, 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.

Сноски

[1]

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

[2]

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

[3]

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

[4]

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

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

Spec-Zone.ru

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