Spec-Zone.ru › Python 3.12

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

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

Вот определение 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;

    /* 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__ для получения строки документации.

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

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);
}
END_OF_DOCUMENT_MARKER ```

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

Интересное преимущество использования таблицы 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,
                 "'%.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 вызывается при необходимости сравнений. Он аналогичен методам сравнения, таким как __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. Флаг может быть установлен для указания наличия слота, но слот может остаться незаполненным.)

Если вы хотите, чтобы ваш объект мог действовать как число, последовательность или объект словаря, поместите адрес структуры, которая реализует C-тип PyNumberMethods, PySequenceMethods или PyMappingMethods соответственно. Вам необходимо заполнить эту структуру соответствующими значениями. Примеры использования каждого из них можно найти в каталоге Objects дистрибутива исходного кода Python.

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

Py_hash_t — это тип целого числа со знаком с платформенно-зависимой длиной. Возврат -1 из tp_hash указывает на ошибку, поэтому следует избегать возврата этого значения при успешном вычислении хэша, как показано выше.

Эта функция вызывается, когда экземпляр вашего типа данных «вызывается», например, если obj1 является экземпляром вашего типа данных, и Python-скрипт содержит obj1('hello'), вызывается обработчик tp_call.

Эта функция принимает три аргумента:

  1. self — это экземпляр типа данных, который является объектом вызова. Если вызов — obj1('hello'), то self — это obj1.
  2. args — кортеж, содержащий аргументы вызова. Вы можете использовать PyArg_ParseTuple() для извлечения аргументов.
  3. kwds — словарь ключевых аргументов, которые были переданы. Если он не пустой, и вы поддерживаете ключевые аргументы, используйте PyArg_ParseTupleAndKeywords() для извлечения аргументов. Если вы не хотите поддерживать ключевые аргументы, и он не пустой, генерируйте TypeError с сообщением о том, что ключевые аргументы не поддерживаются.

Вот пример реализации tp_call

/* 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.

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

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

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

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

Чтобы узнать, как реализовать любой конкретный метод для вашего нового типа данных, обратитесь к исходному коду CPython. Перейдите в каталог Objects, затем найдите в файлах исходного кода C tp_ плюс функцию, которую вы хотите (например, tp_richcompare). Там вы найдете примеры реализации нужной функции.

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

См. также

Загрузка релизов исходного кода CPython.

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

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

https://github.com/python/cpython

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

Spec-Zone.ru

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