3. Визначення типів розширень: різні теми

Цей розділ має на меті дати швидкий огляд методів різних типів, які ви можете застосувати, і того, що вони роблять.

Ось визначення PyTypeObject з деякими полями, які використовуються лише в debug builds опущено:

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

    /* Methods to implement standard operations */

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

    /* Method suites for standard classes */

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

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

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

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

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

    const char *tp_doc; /* Documentation string */

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

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

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

    /* weak reference enabler */
    Py_ssize_t tp_weaklistoffset;

    /* Iterators */
    getiterfunc tp_iter;
    iternextfunc tp_iternext;

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

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

    destructor tp_finalize;
    vectorcallfunc tp_vectorcall;

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

If no tp_repr handler is specified, the interpreter will supply a representation that uses the type’s tp_name and a uniquely identifying value for the object.

Обробник 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 supports two pairs of attribute handlers; a type that supports attributes only needs to implement the functions for one pair. The difference is that one pair takes the name of the attribute as a char*, while the other accepts a PyObject*. Each type can use whichever pair makes more sense for the implementation’s convenience.

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

If accessing attributes of an object is always a simple operation (this will be explained shortly), there are generic implementations which can be used to provide the PyObject* version of the attribute management functions. The actual need for type-specific attribute handlers almost completely disappeared starting with Python 2.2, though there are many examples which have not been updated to use some of the new generic mechanism that is available.

3.3.1. Керування загальними атрибутами

Більшість типів розширень використовують лише прості атрибути. Отже, що робить атрибути простими? Необхідно виконати лише кілька умов:

  1. Під час виклику PyType_Ready() мають бути відомі назви атрибутів.

  2. Ніякої спеціальної обробки не потрібно, щоб записати, що атрибут було знайдено або встановлено, а також не потрібно виконувати дії на основі значення.

Зауважте, що цей список не накладає жодних обмежень на значення атрибутів, час обчислення значень або спосіб зберігання відповідних даних.

Коли викликається PyType_Ready(), він використовує три таблиці, на які посилається об’єкт типу, щоб створити descriptor, які розміщуються в словнику об’єкта типу. Кожен дескриптор керує доступом до одного атрибута об’єкта екземпляра. Кожна з таблиць необов’язкова; якщо всі три мають значення NULL, екземпляри типу матимуть лише атрибути, успадковані від їх базового типу, і повинні залишити поля tp_getattro і tp_setattro як NULL, що дозволяє базовому типу обробляти атрибути.

Таблиці оголошуються як три поля типу object:

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

Якщо tp_methods не є NULL, він має посилатися на масив структур PyMethodDef. Кожен запис у таблиці є екземпляром цієї структури:

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

One entry should be defined for each method provided by the type; no entries are needed for methods inherited from a base type. One additional entry is needed at the end; it is a sentinel that marks the end of the array. The ml_name field of the sentinel must be NULL.

Друга таблиця використовується для визначення атрибутів, які відображаються безпосередньо на дані, що зберігаються в екземплярі. Підтримуються різноманітні примітивні типи C, і доступ може бути лише для читання або читання-запису. Структури в таблиці визначені як:

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

For each entry in the table, a descriptor will be constructed and added to the type which will be able to extract a value from the instance structure. The type field should contain a type code like Py_T_INT or Py_T_DOUBLE; the value will be used to determine how to convert Python values to and from C values. The flags field is used to store flags which control how the attribute can be accessed: you can set it to Py_READONLY to prevent Python code from setting it.

An interesting advantage of using the tp_members table to build descriptors that are used at runtime is that any attribute defined this way can have an associated doc string simply by providing the text in the table. An application can use the introspection API to retrieve the descriptor from the class object, and get the doc string using its __doc__ attribute.

As with the tp_methods table, a sentinel entry with a ml_name value of NULL is required.

3.3.2. Типозалежне керування атрибутами

For simplicity, only the char* version will be demonstrated here; the type of the name parameter is the only difference between the char* and PyObject* flavors of the interface. This example effectively does the same thing as the generic example above, but does not use the generic support added in Python 2.2. It explains how the handler functions are called, so that if you do need to extend their functionality, you’ll understand what needs to be done.

The tp_getattr handler is called when the object requires an attribute look-up. It is called in the same situations where the __getattr__() method of a class would be called.

Ось приклад:

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

The tp_setattr handler is called when the __setattr__() or __delattr__() method of a class instance would be called. When an attribute should be deleted, the third parameter will be NULL. Here is an example that simply raises an exception; if this were really all you wanted, the tp_setattr handler should be set to 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;

The tp_richcompare handler is called when comparisons are needed. It is analogous to the rich comparison methods, like __lt__(), and also called by PyObject_RichCompare() and 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 is a signed integer type with a platform-varying width. Returning -1 from tp_hash indicates an error, which is why you should be careful to avoid returning it when hash computation is successful, as seen above.

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;

These functions provide support for the iterator protocol. Both handlers take exactly one parameter, the instance for which they are being called, and return a new reference. In the case of an error, they should set an exception and return NULL. tp_iter corresponds to the Python __iter__() method, while tp_iternext corresponds to the Python __next__() method.

Будь-який об’єкт iterable повинен реалізовувати обробник tp_iter, який має повертати об’єкт iterator. Тут застосовуються ті самі правила, що й для класів Python:

  • Для колекцій (таких як списки та кортежі), які можуть підтримувати кілька незалежних ітераторів, новий ітератор слід створювати та повертати під час кожного виклику tp_iter.

  • Об’єкти, які можна повторити лише один раз (зазвичай через побічні ефекти ітерації, такі як файлові об’єкти), можуть реалізувати tp_iter, повертаючи нове посилання на себе — і тому також повинні реалізувати tp_iternext обробник.

Будь-який об’єкт iterator повинен реалізовувати як tp_iter, так і tp_iternext. Обробник tp_iter ітератора має повертати нове посилання на ітератор. Його tp_iternext обробник має повернути нове посилання на наступний об’єкт у ітерації, якщо він є. Якщо ітерація досягла кінця, tp_iternext може повернути NULL без встановлення винятку, або він може встановити StopIteration на додаток до повернення NULL; уникнення винятку може дати трохи кращу продуктивність. Якщо сталася фактична помилка, tp_iternext має завжди встановлювати виняток і повертати NULL.

3.6. Слабка довідкова підтримка

Одна з цілей реалізації слабкого посилання Python полягає в тому, щоб дозволити будь-якому типу брати участь у механізмі слабкого посилання без накладних витрат на критичні для продуктивності об’єкти (наприклад, числа).

Дивись також

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

For an object to be weakly referencable, the extension type must set the Py_TPFLAGS_MANAGED_WEAKREF bit of the tp_flags field. The legacy tp_weaklistoffset field should be left as zero.

Concretely, here is how the statically declared type object would look:

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

The only further addition is that tp_dealloc needs to clear any weak references (by calling 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