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

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

Здесь приведено определение PyTypeObject, а некоторые поля, используемые только в debug builds, опущены:

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__, чтобы получить строку 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(), он использует три таблицы, на которые ссылается объект типа, для создания descriptorдескрипторов, которые помещаются в словарь объекта типа. Каждый дескриптор управляет доступом к одному атрибуту объекта экземпляра. Каждая из таблиц необязательна; если все три будут иметь значение NULL, экземпляры типа будут иметь только атрибуты, унаследованные от базового типа, и должны оставить поля tp_getattro и tp_setattro также NULL, позволяя базовому типу работать с атрибутами.

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

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;

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

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

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

3.3.2. Управление атрибутами конкретного типа

Для простоты здесь будет показана только версия char*; тип параметра name - единственное различие между версиями интерфейса 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 вызывается при необходимости сравнения. Он аналогичен rich comparison methods, как и __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() для извлечения аргументов. Если вы не хотите поддерживать аргументы ключевых слов и значение this не``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__().

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

  • Для коллекций (таких как списки и кортежи), которые могут поддерживать несколько независимых итераторов, при каждом вызове tp_iter должен быть создан и возвращен новый итератор.

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

Любой объект iterator должен реализовывать и 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, затем найдите в исходных файлах на языке Си 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