Spec-Zone.ru › Python 3.13

Определение типов расширений: различные темы

В этом разделе кратко рассматриваются различные методы типов, которые можно реализовать, и их назначение.

Вот определение PyTypeObject; некоторые поля, используемые только в отладочных сборках, опущены:

typedef struct _typeobject {
    PyObject_VAR_HEAD
    const char *tp_name; /* For printing, in format "<module>.<name>" */
    Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

    /* Methods to implement standard operations */

    destructor tp_dealloc;
    Py_ssize_t tp_vectorcall_offset;
    getattrfunc tp_getattr;
    setattrfunc tp_setattr;
    PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2)
                                    or tp_reserved (Python 3) */
    reprfunc tp_repr;

    /* Method suites for standard classes */

    PyNumberMethods *tp_as_number;
    PySequenceMethods *tp_as_sequence;
    PyMappingMethods *tp_as_mapping;

    /* More standard operations (here for binary compatibility) */

    hashfunc tp_hash;
    ternaryfunc tp_call;
    reprfunc tp_str;
    getattrofunc tp_getattro;
    setattrofunc tp_setattro;

    /* Functions to access object as input/output buffer */
    PyBufferProcs *tp_as_buffer;

    /* Flags to define presence of optional/expanded features */
    unsigned long tp_flags;

    const char *tp_doc; /* Documentation string */

    /* Assigned meaning in release 2.0 */
    /* call function for all accessible objects */
    traverseproc tp_traverse;

    /* delete references to contained objects */
    inquiry tp_clear;

    /* Assigned meaning in release 2.1 */
    /* rich comparisons */
    richcmpfunc tp_richcompare;

    /* weak reference enabler */
    Py_ssize_t tp_weaklistoffset;

    /* Iterators */
    getiterfunc tp_iter;
    iternextfunc tp_iternext;

    /* Attribute descriptor and subclassing stuff */
    struct PyMethodDef *tp_methods;
    struct PyMemberDef *tp_members;
    struct PyGetSetDef *tp_getset;
    // Strong reference on a heap type, borrowed reference on a static type
    struct _typeobject *tp_base;
    PyObject *tp_dict;
    descrgetfunc tp_descr_get;
    descrsetfunc tp_descr_set;
    Py_ssize_t tp_dictoffset;
    initproc tp_init;
    allocfunc tp_alloc;
    newfunc tp_new;
    freefunc tp_free; /* Low-level free-memory routine */
    inquiry tp_is_gc; /* For PyObject_IS_GC */
    PyObject *tp_bases;
    PyObject *tp_mro; /* method resolution order */
    PyObject *tp_cache;
    PyObject *tp_subclasses;
    PyObject *tp_weaklist;
    destructor tp_del;

    /* Type attribute cache version tag. Added in version 2.6 */
    unsigned int tp_version_tag;

    destructor tp_finalize;
    vectorcallfunc tp_vectorcall;

    /* bitset of which type-watchers care about this type */
    unsigned char tp_watched;
} PyTypeObject;

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

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

const char *tp_name; /* For printing */

Имя типа — как упоминалось в предыдущей главе, оно будет отображаться в различных местах, почти исключительно в диагностических целях. Постарайтесь выбрать имя, которое будет полезно в таких случаях!

Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

Эти поля сообщают среде выполнения, сколько памяти выделять при создании новых объектов этого типа. В Python есть встроенная поддержка структур переменной длины (например, строк и кортежей), для которых используется поле tp_itemsize. Мы рассмотрим его позже.

const char *tp_doc;

Здесь можно указать строку (или её адрес), которая будет возвращаться, когда сценарий Python обращается к obj.__doc__ для получения строки документации.

Теперь перейдём к основным методам типа — тем, которые реализует большинство типов расширений.

3.1. Финализация и освобождение памяти

destructor tp_dealloc;

Эта функция вызывается, когда счётчик ссылок экземпляра вашего типа уменьшается до нуля и интерпретатор Python хочет освободить его. Если для вашего типа нужно освободить память или выполнить другую очистку, это можно сделать здесь. Сам объект тоже необходимо освободить здесь. Вот пример такой функции:

static void
newdatatype_dealloc(newdatatypeobject *obj)
{
    free(obj->obj_UnderlyingDatatypePtr);
    Py_TYPE(obj)->tp_free((PyObject *)obj);
}

Если ваш тип поддерживает сборку мусора, перед очисткой любых полей-членов деструктор должен вызвать PyObject_GC_UnTrack():

static void
newdatatype_dealloc(newdatatypeobject *obj)
{
    PyObject_GC_UnTrack(obj);
    Py_CLEAR(obj->other_obj);
    ...
    Py_TYPE(obj)->tp_free((PyObject *)obj);
}

Одно из важных требований к функции освобождения памяти — она не должна затрагивать ожидающие исключения. Это важно, поскольку деаллокаторы часто вызываются при раскрутке стека Python интерпретатором; если стек раскручивается из-за исключения (а не при нормальном возврате), ничто не защищает деаллокаторы от ситуации, когда исключение уже установлено. Действия, выполняемые деаллокатором и способные привести к выполнению дополнительного кода Python, могут обнаружить, что исключение уже установлено. Это может привести к вводящим в заблуждение сообщениям об ошибках от интерпретатора. Чтобы избежать этого, перед выполнением небезопасного действия нужно сохранить ожидающее исключение, а после его завершения восстановить. Для этого можно использовать функции PyErr_Fetch() и PyErr_Restore():

static void
my_dealloc(PyObject *obj)
{
    MyObject *self = (MyObject *) obj;
    PyObject *cbresult;

    if (self->my_callback != NULL) {
        PyObject *err_type, *err_value, *err_traceback;

        /* This saves the current exception state */
        PyErr_Fetch(&err_type, &err_value, &err_traceback);

        cbresult = PyObject_CallNoArgs(self->my_callback);
        if (cbresult == NULL)
            PyErr_WriteUnraisable(self->my_callback);
        else
            Py_DECREF(cbresult);

        /* This restores the saved exception state */
        PyErr_Restore(err_type, err_value, err_traceback);

        Py_DECREF(self->my_callback);
    }
    Py_TYPE(obj)->tp_free((PyObject*)self);
}

Примечание

Выполнение некоторых действий в функции освобождения памяти небезопасно. Во-первых, если ваш тип поддерживает сборку мусора (с помощью tp_traverse и/или tp_clear), некоторые члены объекта могут быть очищены или финализированы к моменту вызова tp_dealloc. Во-вторых, в tp_dealloc ваш объект находится в нестабильном состоянии: его счётчик ссылок равен нулю. Любой вызов нетривиального объекта или API (как в примере выше) может привести к повторному вызову tp_dealloc, что вызовет двойное освобождение памяти и аварийное завершение работы.

Начиная с Python 3.4 рекомендуется не помещать сложный код финализации в tp_dealloc, а вместо этого использовать новый метод типа tp_finalize.

См. также

В PEP 442 описана новая схема финализации.

3.2. Представление объектов

В Python есть два способа получить текстовое представление объекта: функция repr() и функция str(). (Функция print() просто вызывает str().) Оба этих обработчика необязательны.

reprfunc tp_repr;
reprfunc tp_str;

Обработчик tp_repr должен возвращать строковый объект с представлением экземпляра, для которого он вызван. Вот простой пример:

static PyObject *
newdatatype_repr(newdatatypeobject *obj)
{
    return PyUnicode_FromFormat("Repr-ified_newdatatype{{size:%d}}",
                                obj->obj_UnderlyingDatatypePtr->size);
}

Если обработчик tp_repr не указан, интерпретатор предоставит представление, в котором используются tp_name типа и уникальное значение, идентифицирующее объект.

Обработчик tp_str для str() выполняет ту же роль, что описанный выше обработчик tp_repr для repr(); то есть он вызывается, когда код Python применяет str() к экземпляру вашего объекта. Его реализация очень похожа на функцию tp_repr, но возвращаемая строка предназначена для чтения человеком. Если tp_str не указан, вместо него используется обработчик tp_repr.

Вот простой пример:

static PyObject *
newdatatype_str(newdatatypeobject *obj)
{
    return PyUnicode_FromFormat("Stringified_newdatatype{{size:%d}}",
                                obj->obj_UnderlyingDatatypePtr->size);
}

3.3. Управление атрибутами

Для каждого объекта, поддерживающего атрибуты, соответствующий тип должен предоставлять функции, определяющие, как разрешаются обращения к атрибутам. Нужна функция для получения атрибутов (если они определены) и другая — для установки атрибутов (если установка разрешена). Удаление атрибута — частный случай: новое значение, передаваемое обработчику, равно NULL.

Python поддерживает две пары обработчиков атрибутов; типу, поддерживающему атрибуты, достаточно реализовать функции одной из пар. Разница в том, что одна пара принимает имя атрибута в виде char*, а другая — в виде PyObject*. Каждый тип может выбрать ту пару, которая удобнее для его реализации.

getattrfunc  tp_getattr;        /* char * version */
setattrfunc  tp_setattr;
/* ... */
getattrofunc tp_getattro;       /* PyObject * version */
setattrofunc tp_setattro;

Если доступ к атрибутам объекта всегда является простой операцией (это будет объяснено далее), можно использовать универсальные реализации для предоставления версии функций управления атрибутами, принимающей PyObject*. Начиная с Python 2.2 потребность в обработчиках атрибутов, специфичных для конкретного типа, почти полностью отпала, хотя многие примеры всё ещё не обновлены и не используют некоторые из доступных новых универсальных механизмов.

3.3.1. Универсальное управление атрибутами

Большинство типов расширений используют только простые атрибуты. Что же делает атрибуты простыми? Должны выполняться всего два условия:

  1. Имена атрибутов должны быть известны при вызове PyType_Ready().
  2. Не требуется специальная обработка для фиксации факта поиска или установки атрибута, а также не нужно выполнять действия, зависящие от его значения.

Обратите внимание: этот список не накладывает ограничений на значения атрибутов, время их вычисления или способ хранения связанных данных.

При вызове PyType_Ready() используются три таблицы, на которые ссылается объект типа, чтобы создать дескрипторы и поместить их в словарь объекта типа. Каждый дескриптор управляет доступом к одному атрибуту объекта-экземпляра. Все три таблицы необязательны; если все они равны NULL, у экземпляров типа будут только атрибуты, унаследованные от базового типа, а поля tp_getattro и tp_setattro также следует оставить равными NULL, чтобы обработкой атрибутов занимался базовый тип.

Таблицы объявляются как три поля объекта типа:

struct PyMethodDef *tp_methods;
struct PyMemberDef *tp_members;
struct PyGetSetDef *tp_getset;

Если tp_methods не равен NULL, он должен указывать на массив структур PyMethodDef. Каждая запись таблицы представляет собой экземпляр этой структуры:

typedef struct PyMethodDef {
    const char  *ml_name;       /* method name */
    PyCFunction  ml_meth;       /* implementation function */
    int          ml_flags;      /* flags */
    const char  *ml_doc;        /* docstring */
} PyMethodDef;

Для каждого метода, предоставляемого типом, должна быть определена одна запись; записи для методов, унаследованных от базового типа, не нужны. В конце требуется ещё одна запись — маркер, обозначающий конец массива. Поле ml_name маркера должно быть равно NULL.

Вторая таблица используется для определения атрибутов, непосредственно соответствующих данным, хранящимся в экземпляре. Поддерживаются различные примитивные типы C; доступ может быть только для чтения или для чтения и записи. Структуры в таблице определяются следующим образом:

typedef struct PyMemberDef {
    const char *name;
    int         type;
    int         offset;
    int         flags;
    const char *doc;
} PyMemberDef;

Для каждой записи таблицы будет создан дескриптор и добавлен к типу; он сможет извлекать значение из структуры экземпляра. Поле type должно содержать код типа, например Py_T_INT или Py_T_DOUBLE; по этому значению определяется способ преобразования значений Python в значения C и обратно. Поле flags используется для хранения флагов, управляющих доступом к атрибуту: установите его в Py_READONLY, чтобы запретить коду Python изменять атрибут.

Интересное преимущество использования таблицы tp_members для создания дескрипторов, используемых во время выполнения, заключается в том, что любой атрибут, определённый таким образом, может иметь связанную строку документации — достаточно указать текст в таблице. Приложение может использовать API интроспекции, чтобы получить дескриптор из объекта класса и прочитать строку документации через его атрибут __doc__.

Как и для таблицы tp_methods, требуется запись-маркер со значением NULL в поле ml_name.

3.3.2. Управление атрибутами, специфичное для типа

Для простоты здесь будет показан только вариант с char*; единственное различие между вариантами интерфейса с char* и PyObject* — это тип параметра имени. Этот пример фактически делает то же, что и приведённый выше универсальный пример, но не использует универсальную поддержку, добавленную в Python 2.2. Он объясняет, как вызываются функции-обработчики, чтобы вы понимали, что нужно делать, если потребуется расширить их возможности.

Обработчик tp_getattr вызывается, когда объекту требуется поиск атрибута. Он вызывается в тех же ситуациях, в которых был бы вызван метод класса __getattr__().

Вот пример:

static PyObject *
newdatatype_getattr(newdatatypeobject *obj, char *name)
{
    if (strcmp(name, "data") == 0)
    {
        return PyLong_FromLong(obj->data);
    }

    PyErr_Format(PyExc_AttributeError,
                 "'%.100s' object has no attribute '%.400s'",
                 Py_TYPE(obj)->tp_name, name);
    return NULL;
}

Обработчик tp_setattr вызывается в тех случаях, когда был бы вызван метод экземпляра класса __setattr__() или __delattr__(). При удалении атрибута третий параметр будет равен NULL. Ниже приведён пример, который просто возбуждает исключение; если это действительно всё, что вам нужно, обработчик tp_setattr следует установить в NULL.

static int
newdatatype_setattr(newdatatypeobject *obj, char *name, PyObject *v)
{
    PyErr_Format(PyExc_RuntimeError, "Read-only attribute: %s", name);
    return -1;
}

3.4. Сравнение объектов

richcmpfunc tp_richcompare;

Обработчик tp_richcompare вызывается, когда требуется выполнить сравнение. Он аналогичен методам расширенного сравнения, например __lt__(), а также вызывается функциями PyObject_RichCompare() и PyObject_RichCompareBool().

Эта функция получает в качестве аргументов два объекта Python и оператор, которым может быть Py_EQ, Py_NE, Py_LE, Py_GE, Py_LT или Py_GT. Она должна сравнить два объекта с учётом указанного оператора и вернуть Py_True или Py_False, если сравнение выполнено успешно; Py_NotImplemented, чтобы указать, что сравнение не реализовано и следует попробовать метод сравнения другого объекта; либо NULL, если было установлено исключение.

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

static PyObject *
newdatatype_richcmp(newdatatypeobject *obj1, newdatatypeobject *obj2, int op)
{
    PyObject *result;
    int c, size1, size2;

    /* code to make sure that both arguments are of type
       newdatatype omitted */

    size1 = obj1->obj_UnderlyingDatatypePtr->size;
    size2 = obj2->obj_UnderlyingDatatypePtr->size;

    switch (op) {
    case Py_LT: c = size1 <  size2; break;
    case Py_LE: c = size1 <= size2; break;
    case Py_EQ: c = size1 == size2; break;
    case Py_NE: c = size1 != size2; break;
    case Py_GT: c = size1 >  size2; break;
    case Py_GE: c = size1 >= size2; break;
    }
    result = c ? Py_True : Py_False;
    Py_INCREF(result);
    return result;
 }

3.5. Поддержка абстрактных протоколов

Python поддерживает различные абстрактные «протоколы»; конкретные интерфейсы для работы с ними описаны в разделе Уровень абстрактных объектов.

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

PyNumberMethods   *tp_as_number;
PySequenceMethods *tp_as_sequence;
PyMappingMethods  *tp_as_mapping;

Если вы хотите, чтобы ваш объект мог вести себя как число, последовательность или объект-отображение, поместите адрес структуры, реализующей тип C PyNumberMethods, PySequenceMethods или PyMappingMethods соответственно. Вы должны заполнить эту структуру подходящими значениями. Примеры использования каждой из них можно найти в каталоге Objects дистрибутива исходного кода Python.

hashfunc tp_hash;

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

static Py_hash_t
newdatatype_hash(newdatatypeobject *obj)
{
    Py_hash_t result;
    result = obj->some_size + 32767 * obj->some_number;
    if (result == -1)
       result = -2;
    return result;
}

Py_hash_t — это знаковый целочисленный тип, ширина которого зависит от платформы. Возврат -1 из tp_hash указывает на ошибку, поэтому при успешном вычислении хеша нужно следить, чтобы не вернуть это значение, как показано выше.

ternaryfunc tp_call;

Эта функция вызывается, когда экземпляр вашего типа данных «вызывается». Например, если obj1 — это экземпляр вашего типа данных и сценарий Python содержит obj1('hello'), будет вызван обработчик tp_call.

Эта функция принимает три аргумента:

  1. self — экземпляр типа данных, для которого выполняется вызов. Если вызов имеет вид obj1('hello'), то self равен obj1.
  2. args — кортеж, содержащий аргументы вызова. Для извлечения аргументов можно использовать PyArg_ParseTuple().
  3. kwds — словарь переданных именованных аргументов. Если он не равен NULL и вы поддерживаете именованные аргументы, используйте PyArg_ParseTupleAndKeywords() для их извлечения. Если вы не хотите поддерживать именованные аргументы и этот параметр не равен NULL, возбудите исключение TypeError с сообщением о том, что именованные аргументы не поддерживаются.

Вот простая реализация tp_call:

static PyObject *
newdatatype_call(newdatatypeobject *obj, PyObject *args, PyObject *kwds)
{
    PyObject *result;
    const char *arg1;
    const char *arg2;
    const char *arg3;

    if (!PyArg_ParseTuple(args, "sss:call", &arg1, &arg2, &arg3)) {
        return NULL;
    }
    result = PyUnicode_FromFormat(
        "Returning -- value: [%d] arg1: [%s] arg2: [%s] arg3: [%s]\n",
        obj->obj_UnderlyingDatatypePtr->size,
        arg1, arg2, arg3);
    return result;
}
/* Iterators */
getiterfunc tp_iter;
iternextfunc tp_iternext;

Эти функции обеспечивают поддержку протокола итератора. Оба обработчика принимают ровно один параметр — экземпляр, для которого они вызываются, — и возвращают новую ссылку. В случае ошибки они должны установить исключение и вернуть NULL. tp_iter соответствует методу Python __iter__(), а tp_iternext — методу Python __next__().

Любой итерируемый объект должен реализовать обработчик tp_iter, который должен возвращать объект-итератор. Здесь действуют те же рекомендации, что и для классов Python:

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

Любой объект-итератор должен реализовать и tp_iter, и tp_iternext. Обработчик tp_iter итератора должен возвращать новую ссылку на сам итератор. Его обработчик tp_iternext должен возвращать новую ссылку на следующий объект последовательности, если такой есть. Если итерация достигла конца, tp_iternext может вернуть NULL, не устанавливая исключение, или дополнительно установить StopIteration; в этом случае также возвращается NULL. Отказ от возбуждения исключения может немного повысить производительность. Если произошла реальная ошибка, tp_iternext всегда должен установить исключение и вернуть NULL.

3.6. Поддержка слабых ссылок

Одна из целей реализации слабых ссылок в Python — позволить любому типу участвовать в механизме слабых ссылок, не создавая накладных расходов для объектов, критичных к производительности (например, чисел).

См. также

Документация модуля weakref.

Чтобы на объект можно было создать слабую ссылку, тип расширения должен установить бит Py_TPFLAGS_MANAGED_WEAKREF поля tp_flags. Устаревшее поле tp_weaklistoffset следует оставить равным нулю.

Вот как должен выглядеть статически объявленный объект типа:

static PyTypeObject TrivialType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    /* ... other members omitted for brevity ... */
    .tp_flags = Py_TPFLAGS_MANAGED_WEAKREF | ...,
};

Единственное дополнительное изменение: tp_dealloc должен очищать все слабые ссылки (вызывая PyObject_ClearWeakRefs()):

static void
Trivial_dealloc(TrivialObject *self)
{
    /* Clear weakrefs first before calling any destructors */
    PyObject_ClearWeakRefs((PyObject *) self);
    /* ... remainder of destruction code omitted for brevity ... */
    Py_TYPE(self)->tp_free((PyObject *) self);
}

3.7. Дополнительные рекомендации

Чтобы узнать, как реализовать конкретный метод для нового типа данных, получите исходный код CPython. Перейдите в каталог Objects, а затем найдите в исходных файлах C строку tp_ вместе с именем нужной функции (например, tp_richcompare). Вы найдёте примеры реализации нужной функции.

Если нужно проверить, является ли объект непосредственным экземпляром реализуемого вами типа, используйте функцию PyObject_TypeCheck(). Пример её использования может выглядеть так:

if (!PyObject_TypeCheck(some_object, &MyType)) {
    PyErr_SetString(PyExc_TypeError, "arg #1 not a mything");
    return NULL;
}

См. также

Загрузить выпуски исходного кода CPython.

https://www.python.org/downloads/source/

Проект CPython на GitHub, где разрабатывается исходный код CPython.

https://github.com/python/cpython

© 2001–2025 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.13/extending/newtypes.html

Spec-Zone.ru

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