Документация Python неофициальный перевод
Содержание страницы

3. Определение типов расширений: разные темыDefining Extension Types: Assorted Topics

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

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

typedef struct _typeobject {
    PyObject_VAR_HEAD
    const char *tp_name; /* Для вывода в формате "<module>.<name>" */
    Py_ssize_t tp_basicsize, tp_itemsize; /* Для выделения памяти */

    /* Методы для реализации стандартных операций */

    destructor tp_dealloc;
    Py_ssize_t tp_vectorcall_offset;
    getattrfunc tp_getattr;
    setattrfunc tp_setattr;
    PyAsyncMethods *tp_as_async; /* ранее известный как tp_compare (Python 2)
                                    или tp_reserved (Python 3) */
    reprfunc tp_repr;

    /* Наборы методов для стандартных классов */

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

    /* Дополнительные стандартные операции (здесь для двоичной совместимости) */

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

    /* Функции для доступа к объекту как к буферу ввода/вывода */
    PyBufferProcs *tp_as_buffer;

    /* Флаги для определения наличия опциональных/расширенных возможностей */
    unsigned long tp_flags;

    const char *tp_doc; /* Строка документации */

    /* Назначенное значение в версии 2.0 */
    /* вызов функции для всех доступных объектов */
    traverseproc tp_traverse;

    /* удаление ссылок на содержащиеся объекты */
    inquiry tp_clear;

    /* Назначенное значение в версии 2.1 */
    /* расширенные сравнения */
    richcmpfunc tp_richcompare;

    /* включение слабых ссылок */
    Py_ssize_t tp_weaklistoffset;

    /* Итераторы */
    getiterfunc tp_iter;
    iternextfunc tp_iternext;

    /* Дескриптор атрибутов и механизмы подклассов */
    struct PyMethodDef *tp_methods;
    struct PyMemberDef *tp_members;
    struct PyGetSetDef *tp_getset;
    // Сильная ссылка на тип в куче, заимствованная ссылка на статический тип
    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; /* Низкоуровневая процедура освобождения памяти */
    inquiry tp_is_gc; /* Для PyObject_IS_GC */
    PyObject *tp_bases;
    PyObject *tp_mro; /* порядок разрешения методов */
    PyObject *tp_cache;
    PyObject *tp_subclasses;
    PyObject *tp_weaklist;
    destructor tp_del;

    /* Метка версии кэша атрибутов типа. Добавлено в версии 2.6. */
    unsigned int tp_version_tag;

    destructor tp_finalize;
    vectorcallfunc tp_vectorcall;

    /* битовый набор, указывающий, какие наблюдатели типов следят за этим типом */
    unsigned char tp_watched;
} PyTypeObject;

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

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

const char *tp_name; /* Для вывода */

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

Py_ssize_t tp_basicsize, tp_itemsize; /* Для выделения памяти */

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

const char *tp_doc;

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

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

3.1. Завершение и освобождение памятиFinalization and De-allocation

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;

        /* Сохраняет текущее состояние исключения */
        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);

        /* Восстанавливает сохранённое состояние исключения */
        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. Представление объектовObject Presentation

В 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. Управление атрибутамиAttribute Management

Для каждого объекта, который может поддерживать атрибуты, соответствующий тип должен предоставлять функции, управляющие разрешением атрибутов. Нужна функция, которая может получать атрибуты (если они определены), и другая – для установки атрибутов (если установка разрешена). Удаление атрибута – особый случай: новое значение, передаваемое обработчику, равно 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. Общее управление атрибутамиGeneric Attribute Management

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

  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;       /* имя метода */
    PyCFunction  ml_meth;       /* функция реализации */
    int          ml_flags;      /* флаги */
    const char  *ml_doc;        /* докстринг */
} 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, требуется сигнальная запись со значением ml_name равным NULL.

3.3.2. Управление атрибутами для конкретного типаType-specific Attribute Management

Для простоты здесь будет показана только версия с char*; единственное различие между вариантами интерфейса char* и PyObject* заключается в типе параметра name. Этот пример делает по сути то же самое, что и общий пример выше, но не использует общую поддержку, добавленную в 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. Сравнение объектовObject Comparison

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;

    /* код для проверки, что оба аргумента имеют нужный тип
       newdatatype опущен */

    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. Поддержка абстрактных протоколовAbstract Protocol Support

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;
}
/* Итераторы */
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. Поддержка слабых ссылокWeak Reference Support

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

См. также

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

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

Конкретно, вот как будет выглядеть статически объявленный объект типа:

static PyTypeObject TrivialType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    /* ... остальные поля опущены для краткости ... */
    .tp_flags = Py_TPFLAGS_MANAGED_WEAKREF | ...,
};

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

static void
Trivial_dealloc(TrivialObject *self)
{
    /* Сначала очистить слабые ссылки перед вызовом любых деструкторов */
    PyObject_ClearWeakRefs((PyObject *) self);
    /* ... остальной код уничтожения опущен для краткости ... */
    Py_TYPE(self)->tp_free((PyObject *) self);
}

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

Чтобы научиться реализовывать любой конкретный метод для вашего нового типа данных, возьмите исходный код 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