Определение типов расширений: разные темы
В этом разделе кратко рассматриваются различные методы типов, которые можно реализовать, и объясняется, что они делают.
Вот определение 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. Универсальное управление атрибутами
Большинство типов расширений используют только простые атрибуты. Что же делает атрибуты простыми? Должны выполняться всего несколько условий:
- Имена атрибутов должны быть известны при вызове
PyType_Ready(). - Для регистрации обращения к атрибуту или его установки не требуется специальная обработка, а также не нужно выполнять действия, зависящие от значения.
Обратите внимание: этот список не накладывает ограничений на значения атрибутов, момент вычисления значений или способ хранения связанных данных.
При вызове 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.
Эта функция принимает три аргумента:
-
self — экземпляр типа данных, к которому применяется вызов. Если вызов —
obj1('hello'), то self равенobj1. -
args — кортеж, содержащий аргументы вызова. Для извлечения аргументов можно использовать
PyArg_ParseTuple(). -
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.
- Проект CPython на GitHub, где разрабатывается исходный код CPython.
© 2001–2024 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.12/extending/newtypes.html