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

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, требуется завершающая запись со значением NULL в поле ml_name.

3.3.2. Типоспецифичное управление атрибутами

Для простоты здесь будет показан только вариант со char*; тип параметра имени — единственное различие между вариантами интерфейса со 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. Флаг может указывать на наличие слота, даже если сам слот ещё не заполнен.)

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 *obj, PyObject *args, PyObject *kwds)
{
    PyObject *result;
    const char *arg1;
    const char *arg2;
    const char *arg3;

    if (!PyArg_ParseTuple(args, "sss:call", &arg1, &arg2, &arg3)) {
        return NULL;
    }
    result = PyUnicode_FromFormat(
        "Returning -- value: [%d] arg1: [%s] arg2: [%s] arg3: [%s]\n",
        obj->obj_UnderlyingDatatypePtr->size,
        arg1, arg2, arg3);
    return result;
}
/* Iterators */
getiterfunc tp_iter;
iternextfunc tp_iternext;

Эти функции обеспечивают поддержку протокола итератора. Каждый из двух обработчиков принимает ровно один параметр — экземпляр, для которого он вызывается, — и возвращает новую ссылку. В случае ошибки они должны установить исключение и вернуть NULL. tp_iter соответствует методу Python __iter__(), а tp_iternext — методу Python __next__().

Любой объект итерируемого типа должен реализовать обработчик 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 следует оставить равным нулю.

Вот как должен выглядеть статически объявленный объект типа:

static PyTypeObject TrivialType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    /* ... other members omitted for brevity ... */
    .tp_flags = Py_TPFLAGS_MANAGED_WEAKREF | ...,
};

Остаётся только добавить, что tp_dealloc должен очищать все слабые ссылки (вызывая PyObject_ClearWeakRefs()):

static void
Trivial_dealloc(TrivialObject *self)
{
    /* Clear weakrefs first before calling any destructors */
    PyObject_ClearWeakRefs((PyObject *) self);
    /* ... remainder of destruction code omitted for brevity ... */
    Py_TYPE(self)->tp_free((PyObject *) self);
}

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

Чтобы узнать, как реализовать конкретный метод для нового типа данных, получите исходный код CPython. Перейдите в каталог Objects, а затем выполните поиск по исходным файлам 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–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