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;
} 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 должно содержать один из кодов типов, определенных в заголовке structmember.h; это значение будет использоваться для определения способа преобразования значений Python в значения C и обратно. Поле flags используется для хранения флагов, которые управляют доступом к атрибуту.

Следующие константы флагов определены в structmember.h; они могут быть объединены с помощью побитового ИЛИ.

Постоянная

Значение

READONLY

Не подлежит записи.

PY_AUDIT_READ

Перед чтением выдайте object.__getattr__ audit events.

Изменено в версии 3.10: RESTRICTED, READ_RESTRICTED и WRITE_RESTRICTED устарели. Однако READ_RESTRICTED является псевдонимом для PY_AUDIT_READ, поэтому поля, в которых указаны RESTRICTED или READ_RESTRICTED, также будут вызывать событие аудита.

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

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

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

Для простоты здесь будет продемонстрирована только версия char*; тип параметра name является единственным различием между интерфейсами char* и PyObject*. Этот пример фактически делает то же самое, что и пример generic выше, но не использует поддержку generic, добавленную в 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,
                 "'%.50s' object has no attribute '%.400s'",
                 tp->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(PyObject *obj1, PyObject *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 *self, 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.

Для того чтобы объект был слабо ссылаемым, тип расширения должен выполнять две вещи:

  1. Включите в структуру объекта Си поле PyObject*, предназначенное для механизма слабых ссылок. Конструктор объекта должен оставить его NULL (что происходит автоматически при использовании по умолчанию tp_alloc).

  2. Установите член типа tp_weaklistoffset на смещение вышеупомянутого поля в структуре объекта C, чтобы интерпретатор знал, как получить доступ и изменить это поле.

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

typedef struct {
    PyObject_HEAD
    PyObject *weakreflist;  /* List of weak references */
} TrivialObject;

И соответствующий член в статически объявленном типе object:

static PyTypeObject TrivialType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    /* ... other members omitted for brevity ... */
    .tp_weaklistoffset = offsetof(TrivialObject, weakreflist),
};

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

static void
Trivial_dealloc(TrivialObject *self)
{
    /* Clear weakrefs first before calling any destructors */
    if (self->weakreflist != NULL)
        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

Вернуться на верх