Spec-Zone.ru › Python 3.11

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

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

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

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

    /* Methods to implement standard operations */

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

    /* Method suites for standard classes */

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

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

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

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

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

    const char *tp_doc; /* Documentation string */

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

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

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

    /* weak reference enabler */
    Py_ssize_t tp_weaklistoffset;

    /* Iterators */
    getiterfunc tp_iter;
    iternextfunc tp_iternext;

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

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

    destructor tp_finalize;
    vectorcallfunc tp_vectorcall;
} PyTypeObject;

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

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

const char *tp_name; /* For printing */

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

Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

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

const char *tp_doc;

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

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

3.1. Заключительная обработка и освобождение

destructor tp_dealloc;

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

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

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

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

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

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

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

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

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

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

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

Примечание

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

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

См. также

PEP 442 объясняет новую схему завершения.

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

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

reprfunc tp_repr;
reprfunc tp_str;

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

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

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

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

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

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

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

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

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

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

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

3.3.1. Управление атрибутами (общий случай)

Большинство типов расширений используют только простые атрибуты. Итак, что делает атрибуты простыми? Существует всего несколько условий, которые должны быть выполнены:

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

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

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

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

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

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

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

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

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

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

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

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

Константа

Значение

READONLY

Никогда не записываемый.

PY_AUDIT_READ

Выдать object.__getattr__ события аудита перед чтением.

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

Интересное преимущество использования таблицы tp_members для построения дескрипторов, используемых во время выполнения, заключается в том, что любой атрибут, определенный таким образом, может иметь связанную строку документации, просто предоставив текст в таблице. Приложение может использовать API интроспекции для извлечения дескриптора из объекта класса и получить строку документации, используя его атрибут __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,
                 "'%.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_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() для извлечения аргументов. Если вы не хотите поддерживать ключевые аргументы и это не 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__().

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

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

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

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

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

См. также

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

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

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

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

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

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

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, затем найдите в файлах C-исходного кода tp_, плюс функцию, которую вы хотите (например, tp_richcompare). Вы найдете примеры реализации желаемой функции.

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

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

См. также

Скачать релизы исходного кода CPython.

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

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

https://github.com/python/cpython

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

Spec-Zone.ru

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