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

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

Вот определение 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)
                                    or 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; /* Строка документации */

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

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

    /* богатые сравнения */
    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;

} PyTypeObject;

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

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

const char *tp_name; /* Для печати */

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

Py_ssize_t tp_basicsize, tp_itemsize; /* Для распределения */

Поля указывают среде выполнения объем памяти, выделяемой при создании новых объектов данного типа. У 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(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_CallObject(self->my_callback, NULL);
        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. Представление объекта

В 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 * версия */
setattrfunc  tp_setattr;
/* ... */
getattrofunc tp_getattro;       /* PyObject * версия */
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;       /* имя метода */
    PyCFunction  ml_meth;       /* функция реализации */
    int          ml_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 должно содержать одно из кодов типов, определенных в заголовке structmember.h; значение будет использоваться для определения способа преобразования Python значения в и из C значения. Поле flags используется для хранения флагов, управляющих доступом к атрибуту.

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

Константа Смысл
READONLY Никогда не может быть записано.
READ_RESTRICTED Недоступно для чтения в ограниченном режиме.
WRITE_RESTRICTED Невозможно выполнить запись в ограниченном режиме.
RESTRICTED Недоступно для чтения или записи в ограниченном режиме.

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

Как и tp_methods таблице, требуется дозорная запись с 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,
                 "'%.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 обработчик вызывается при необходимости сравнения. Он аналогичен богатым методам сравнения, как и __lt__(), а также вызывается PyObject_RichCompare() и PyObject_RichCompareBool().

Данная функция вызывается с двумя объектами Python и оператором в качестве аргументов, где оператор является одним из Py_EQ, Py_NE, Py_LE, Py_GT, 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;

   /* код убедиться, что оба аргумента типа
       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. Абстрактная поддержка протокола

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 *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;
}
/* Итераторы */
getiterfunc tp_iter;
iternextfunc tp_iternext;

Эти функции обеспечивают поддержку протокола итератора. Оба обработчика принимают ровно один параметр, сущность, для которого они вызываются, и возвращает новую ссылку. В случае ошибки они должны установить исключение и возвращает NULL.:c:member:~PyTypeObject.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; исключение может yield несколько лучшую производительность. Если фактическая ошибка происходит, tp_iternext всегда должна устанавливать исключение и возвращает NULL.

3.6. Слабая справочная поддержка

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

См.также

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

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

  1. Включите поле PyObject* в структуру объекта C, предназначенную для слабого ссылочного механизма. Конструктор объекта должен оставить его NULL (что автоматически при использовании значения по умолчанию: c:member:~PyTypeObject.tp_alloc).
  2. Установите tp_weaklistoffset элемент типа смещение вышеупомянутого поля в структуре объекта C, чтобы интерпретатор знал, как получить доступ к этому полю и изменить его.

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

typedef struct {
    PyObject_HEAD
    PyObject *weakreflist;  /* Список слабых ссылок */
} TrivialObject;

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

static PyTypeObject TrivialType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    /* ... другие члены пропущены для краткости ... */
    .tp_weaklistoffset = offsetof(TrivialObject, weakreflist),
};

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

static void
Trivial_dealloc(TrivialObject *self)
{
    /* Прежде чем вызывать деструкторы, очистите слабые ссылки */
    if (self->weakreflist != NULL)
        PyObject_ClearWeakRefs((PyObject *) self);
    /* ... остальная часть кода уничтожения пропущена для краткости ... */
    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 исходники релизов.
Исходники
Проект CPython на GitHub, где разрабатывается CPython исходный код.
Домашняя страница