Определение типов расширений: разные темы
В этом разделе кратко рассматриваются различные методы типов, которые можно реализовать, и их назначение.
Вот определение 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 */
PyMethodDef *tp_methods;
PyMemberDef *tp_members;
PyGetSetDef *tp_getset;
// Strong reference on a heap type, borrowed reference on a static type
PyTypeObject *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; /* no longer used */
void *tp_subclasses; /* for static builtin types this is an index */
PyObject *tp_weaklist; /* not used for static builtin types */
destructor tp_del;
/* Type attribute cache version tag. Added in version 2.6.
* If zero, the cache is invalid and must be initialized.
*/
unsigned int tp_version_tag;
destructor tp_finalize;
vectorcallfunc tp_vectorcall;
/* bitset of which type-watchers care about this type */
unsigned char tp_watched;
/* Number of tp_version_tag values used.
* Set to _Py_ATTR_CACHE_UNUSED if the attribute cache is
* disabled for this type (e.g. due to custom MRO entries).
* Otherwise, limited to MAX_VERSIONS_PER_CLASS (defined elsewhere).
*/
uint16_t tp_versions_used;
} 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(PyObject *op)
{
newdatatypeobject *self = (newdatatypeobject *) op;
free(self->obj_UnderlyingDatatypePtr);
Py_TYPE(self)->tp_free(self);
}
Если ваш тип поддерживает сборку мусора, деструктор должен вызывать PyObject_GC_UnTrack() до очистки любых полей-членов:
static void
newdatatype_dealloc(PyObject *op)
{
newdatatypeobject *self = (newdatatypeobject *) op;
PyObject_GC_UnTrack(op);
Py_CLEAR(self->other_obj);
...
Py_TYPE(self)->tp_free(self);
}
Одно из важных требований к функции освобождения памяти — она должна оставлять все ожидающие исключения без изменений. Это важно, поскольку функции освобождения памяти часто вызываются при разматывании стека 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(self)->tp_free(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(PyObject *op)
{
newdatatypeobject *self = (newdatatypeobject *) op;
return PyUnicode_FromFormat("Repr-ified_newdatatype{{size:%d}}",
self->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(PyObject *op)
{
newdatatypeobject *self = (newdatatypeobject *) op;
return PyUnicode_FromFormat("Stringified_newdatatype{{size:%d}}",
self->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(PyObject *op, char *name)
{
newdatatypeobject *self = (newdatatypeobject *) op;
if (strcmp(name, "data") == 0) {
return PyLong_FromLong(self->data);
}
PyErr_Format(PyExc_AttributeError,
"'%.100s' object has no attribute '%.400s'",
Py_TYPE(self)->tp_name, name);
return NULL;
}
Обработчик tp_setattr вызывается в тех случаях, когда был бы вызван метод __setattr__() или __delattr__() экземпляра класса. При удалении атрибута третий параметр будет равен NULL. Вот пример, который просто вызывает исключение; если это всё, что вам нужно, обработчик tp_setattr следует установить в NULL.
static int
newdatatype_setattr(PyObject *op, 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 *lhs, PyObject *rhs, int op)
{
newdatatypeobject *obj1 = (newdatatypeobject *) lhs;
newdatatypeobject *obj2 = (newdatatypeobject *) rhs;
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;
return Py_NewRef(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(PyObject *op)
{
newdatatypeobject *self = (newdatatypeobject *) op;
Py_hash_t result;
result = self->some_size + 32767 * self->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(PyObject *op, PyObject *args, PyObject *kwds)
{
newdatatypeobject *self = (newdatatypeobject *) op;
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",
self->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(PyObject *op)
{
/* Clear weakrefs first before calling any destructors */
PyObject_ClearWeakRefs(op);
/* ... remainder of destruction code omitted for brevity ... */
Py_TYPE(op)->tp_free(op);
}
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 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.14/extending/newtypes.html