Изоляция модулей расширений
Для кого предназначено руководство
Это руководство предназначено для сопровождающих расширений C-API, которые хотят сделать эти расширения безопаснее для использования в приложениях, где сам Python применяется как библиотека.
Общие сведения
Интерпретатор — это контекст, в котором выполняется код Python. Он содержит конфигурацию (например, путь импорта) и состояние среды выполнения (например, набор импортированных модулей).
Python поддерживает запуск нескольких интерпретаторов в одном процессе. Следует учитывать два случая — интерпретаторы могут запускаться:
- последовательно, с несколькими циклами
Py_InitializeEx()/Py_FinalizeEx(), и - параллельно, с управлением «субинтерпретаторами» с помощью
Py_NewInterpreter()/Py_EndInterpreter().
Оба случая (и их сочетания) особенно полезны при встраивании Python в библиотеку. Библиотеки, как правило, не должны делать предположений о приложении, которое их использует, в том числе предполагать наличие единственного «главного интерпретатора Python» на уровне процесса.
Исторически модули расширений Python плохо справляются с этим сценарием использования. Во многих модулях расширений (и даже в некоторых модулях стандартной библиотеки) используется глобальное состояние на уровне процесса, поскольку переменными C static чрезвычайно просто пользоваться. В результате данные, которые должны быть специфичны для интерпретатора, оказываются общими для нескольких интерпретаторов. Если разработчик расширения не проявит осторожность, очень легко допустить пограничные случаи, приводящие к сбоям, когда модуль загружается более чем в одном интерпретаторе одного процесса.
К сожалению, состояние на уровне интерпретатора реализовать непросто. Разработчики расширений обычно не учитывают возможность работы с несколькими интерпретаторами, а тестировать такое поведение сейчас затруднительно.
Состояние на уровне модуля
Вместо того чтобы сосредоточиться на состоянии на уровне интерпретатора, C API Python развивается в направлении более эффективной поддержки более детализированного состояния на уровне модуля. Это означает, что данные уровня C должны быть связаны с объектом модуля. Каждый интерпретатор создаёт собственный объект модуля, благодаря чему данные остаются раздельными. Чтобы проверить изоляцию, в одном интерпретаторе можно даже загрузить несколько объектов модулей, соответствующих одному расширению.
Состояние на уровне модуля упрощает понимание времени жизни и владения ресурсами: модуль расширения инициализируется при создании объекта модуля и очищается при его освобождении. В этом отношении модуль ничем не отличается от любого другого PyObject*; не нужно помнить о каких-либо обработчиках «при завершении работы интерпретатора» — и невозможно о них забыть.
Обратите внимание, что для разных видов «глобальных» данных могут существовать сценарии использования: состояние на уровне процесса, интерпретатора, потока или задачи. Если состоянием по умолчанию является состояние на уровне модуля, такие варианты всё ещё возможны, но их следует считать исключительными случаями: если они вам нужны, уделите им особое внимание и тщательно протестируйте. (В этом руководстве они не рассматриваются.)
Изолированные объекты модулей
При разработке модуля расширения важно помнить, что из одной общей разделяемой библиотеки можно создать несколько объектов модулей. Например:
>>> import sys >>> import binascii >>> old_binascii = binascii >>> del sys.modules['binascii'] >>> import binascii # create a new module object >>> old_binascii == binascii False
Как правило, эти два модуля должны быть полностью независимыми. Все объекты и состояние, специфичные для модуля, должны быть инкапсулированы в объекте модуля, а не совместно использоваться с другими объектами модулей, и очищаться при освобождении объекта модуля. Поскольку это лишь общее правило, возможны исключения (см. раздел Управление глобальным состоянием), но они требуют более тщательного обдумывания и внимания к пограничным случаям.
Некоторым модулям могли бы подойти менее строгие ограничения, но изолированные модули позволяют сформулировать чёткие ожидания и рекомендации, подходящие для самых разных сценариев использования.
Неожиданные пограничные случаи
Обратите внимание: изолированные модули могут приводить к некоторым неожиданным пограничным случаям. В частности, каждый объект модуля обычно не разделяет свои классы и исключения с другими аналогичными модулями. Продолжая пример выше, обратите внимание, что old_binascii.Error и binascii.Error — это разные объекты. В следующем коде исключение не перехватывается:
>>> old_binascii.Error == binascii.Error
False
>>> try:
... old_binascii.unhexlify(b'qwertyuiop')
... except binascii.Error:
... print('boo')
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
binascii.Error: Non-hexadecimal digit found
Так и должно быть. Обратите внимание, что модули на чистом Python ведут себя так же: это часть принципов работы Python.
Цель состоит в том, чтобы сделать модули расширений безопасными на уровне C, а не в том, чтобы хаки вели себя интуитивно. «Ручное» изменение sys.modules считается хаком.
Обеспечение безопасности модулей при работе с несколькими интерпретаторами
Управление глобальным состоянием
Иногда состояние, связанное с модулем Python, относится не к этому модулю, а ко всему процессу (или к чему-то ещё, что является «более глобальным», чем модуль). Например:
- Модуль
readlineуправляет терминалом. - Модуль, работающий на печатной плате, хочет управлять встроенным светодиодом.
В этих случаях модуль Python должен предоставлять доступ к глобальному состоянию, а не владеть им. По возможности напишите модуль так, чтобы несколько его копий могли независимо обращаться к этому состоянию (наряду с другими библиотеками, как для Python, так и для других языков). Если это невозможно, рассмотрите возможность явной блокировки.
Если необходимо использовать глобальное состояние процесса, самый простой способ избежать проблем с несколькими интерпретаторами — явно запретить загружать модуль более одного раза в одном процессе; см. раздел Отказ от поддержки: ограничение одним объектом модуля на процесс.
Управление состоянием на уровне модуля
Чтобы использовать состояние на уровне модуля, воспользуйтесь многофазной инициализацией модуля расширения. Это означает, что ваш модуль корректно поддерживает несколько интерпретаторов.
Установите PyModuleDef.m_size в положительное число, чтобы запросить такое количество байтов хранилища, локального для модуля. Обычно это значение устанавливают равным размеру некоторой специфичной для модуля struct, в которой можно хранить всё состояние модуля на уровне C. В частности, там следует хранить указатели на классы (включая исключения, но не статические типы) и настройки (например, csv — field_size_limit), необходимые для работы кода C.
Примечание
Ещё один вариант — хранить состояние в __dict__ модуля, но необходимо избегать сбоев, если пользователи изменяют __dict__ из кода Python. Обычно для этого требуется проверять ошибки и типы на уровне C; такие проверки легко реализовать неправильно и сложно достаточно тщательно протестировать.
Однако если состояние модуля не требуется коду C, хранить его только в __dict__ — хорошая идея.
Если состояние модуля содержит указатели PyObject, объект модуля должен хранить ссылки на эти объекты и реализовывать обработчики на уровне модуля m_traverse, m_clear и m_free. Они работают так же, как tp_traverse, tp_clear и tp_free класса. Их добавление потребует некоторой работы и увеличит объём кода; такова цена за возможность корректно выгружать модули.
Пример модуля с состоянием на уровне модуля доступен в файле xxlimited; пример инициализации модуля приведён в конце файла.
Отказ от поддержки: ограничение одним объектом модуля на процесс
Неотрицательное значение PyModuleDef.m_size означает, что модуль корректно поддерживает несколько интерпретаторов. Если ваш модуль пока не поддерживает несколько интерпретаторов, можно явно разрешить его загрузку только один раз в одном процессе. Например:
// A process-wide flag
static int loaded = 0;
// Mutex to provide thread safety (only needed for free-threaded Python)
static PyMutex modinit_mutex = {0};
static int
exec_module(PyObject* module)
{
PyMutex_Lock(&modinit_mutex);
if (loaded) {
PyMutex_Unlock(&modinit_mutex);
PyErr_SetString(PyExc_ImportError,
"cannot load module more than once per process");
return -1;
}
loaded = 1;
PyMutex_Unlock(&modinit_mutex);
// ... rest of initialization
}
Если функция PyModuleDef.m_clear вашего модуля способна подготовить его к повторной инициализации, ей следует сбросить флаг loaded. В этом случае модуль не будет поддерживать одновременное существование нескольких экземпляров, но, например, его можно будет загрузить после завершения работы среды выполнения Python (Py_FinalizeEx()) и повторно инициализировать её (Py_Initialize()).
Доступ к состоянию модуля из функций
Получить доступ к состоянию из функций уровня модуля несложно. Функции получают объект модуля в качестве первого аргумента; для извлечения состояния можно использовать PyModule_GetState:
static PyObject *
func(PyObject *module, PyObject *args)
{
my_struct *state = (my_struct*)PyModule_GetState(module);
if (state == NULL) {
return NULL;
}
// ... rest of logic
}
Примечание
PyModule_GetState может вернуть NULL, не устанавливая исключение, если состояние модуля отсутствует, то есть если PyModuleDef.m_size равно нулю. В собственном модуле вы контролируете m_size, поэтому этого легко избежать.
Типы в куче
Традиционно типы, определённые в коде C, являются статическими, то есть представляют собой структуры static PyTypeObject, непосредственно определённые в коде и инициализированные с помощью PyType_Ready().
Такие типы неизбежно являются общими для всего процесса. Чтобы использовать их совместно в нескольких объектах модулей, необходимо учитывать любое состояние, которым они владеют или к которому обращаются. Чтобы ограничить возможные проблемы, статические типы неизменяемы на уровне Python: например, нельзя установить str.myattribute = 123.
Особенность реализации CPython: Совместное использование действительно неизменяемых объектов между интерпретаторами допустимо, если они не предоставляют доступ к изменяемым объектам. Однако в CPython у каждого объекта Python есть изменяемая особенность реализации: счётчик ссылок. Изменения счётчика ссылок защищены GIL. Таким образом, код, совместно использующий объекты Python между интерпретаторами, неявно зависит от текущего GIL CPython, действующего на уровне всего процесса.
Поскольку статические типы неизменяемы и существуют на уровне всего процесса, они не могут обращаться к «состоянию своего модуля». Если какому-либо методу такого типа требуется доступ к состоянию модуля, тип необходимо преобразовать в тип, размещаемый в куче, или, кратко, тип в куче. Такие типы больше похожи на классы, создаваемые инструкцией class в Python.
Для новых модулей хорошим общим правилом будет использовать по умолчанию типы в куче.
Преобразование статических типов в типы в куче
Статические типы можно преобразовать в типы в куче, но учтите, что API типов в куче не разрабатывался для «без потерь» преобразования статических типов, то есть для создания типа, работающего в точности как заданный статический тип. Поэтому при переписывании определения класса с использованием нового API вы, скорее всего, непреднамеренно измените некоторые детали (например, возможность сериализации с помощью pickle или унаследованные слоты). Обязательно проверьте важные для вас детали.
Обратите особое внимание на следующие два момента (но имейте в виду, что это не исчерпывающий список):
- В отличие от статических типов, объекты типов в куче по умолчанию изменяемы. Используйте флаг
Py_TPFLAGS_IMMUTABLETYPE, чтобы запретить изменение. - Типы в куче по умолчанию наследуют
tp_new, поэтому их можно будет создавать из кода Python. Это можно запретить с помощью флагаPy_TPFLAGS_DISALLOW_INSTANTIATION.
Определение типов в куче
Типы в куче можно создать, заполнив структуру PyType_Spec — описание, или «чертёж» класса — и вызвав PyType_FromModuleAndSpec() для создания нового объекта класса.
Примечание
Типы в куче можно создавать и с помощью других функций, например PyType_FromSpec(), но PyType_FromModuleAndSpec() связывает класс с модулем, позволяя обращаться к состоянию модуля из методов.
Как правило, класс следует хранить и там, и там: в состоянии модуля (для безопасного доступа из C) и в __dict__ модуля (для доступа из кода Python).
Протокол сборки мусора
Экземпляры типов в куче хранят ссылку на свой тип. Это гарантирует, что тип не будет уничтожен до уничтожения всех его экземпляров, но может привести к циклам ссылок, которые должен разрывать сборщик мусора.
Чтобы избежать утечек памяти, экземпляры типов в куче должны реализовывать протокол сборки мусора. То есть типы в куче должны:
- Иметь флаг
Py_TPFLAGS_HAVE_GC. - Определять функцию обхода с помощью
Py_tp_traverse, которая посещает тип (например, с помощьюPy_VISIT(Py_TYPE(self))).
Дополнительные сведения см. в документации по Py_TPFLAGS_HAVE_GC и tp_traverse.
API для определения типов в куче развивался постепенно, поэтому сейчас им несколько неудобно пользоваться. В следующих разделах рассматриваются распространённые проблемы.
tp_traverse в Python 3.8 и более ранних версиях
Требование посещать тип из tp_traverse появилось в Python 3.9. Если вы поддерживаете Python 3.8 и более ранние версии, функция обхода не должна посещать тип, поэтому она должна быть сложнее:
static int my_traverse(PyObject *self, visitproc visit, void *arg)
{
if (Py_Version >= 0x03090000) {
Py_VISIT(Py_TYPE(self));
}
return 0;
}
К сожалению, Py_Version появилась только в Python 3.11. Вместо неё используйте:
-
PY_VERSION_HEX, если не используете стабильный ABI, или -
sys.version_info(черезPySys_GetObject()иPyArg_ParseTuple()).
Делегирование tp_traverse
Если ваша функция обхода делегирует вызов tp_traverse базового класса (или другого типа), убедитесь, что Py_TYPE(self) посещается только один раз. Учтите, что посещать тип в tp_traverse должны только типы в куче.
Например, если функция обхода содержит:
base->tp_traverse(self, visit, arg)
…и base может быть статическим типом, она также должна содержать:
if (base->tp_flags & Py_TPFLAGS_HEAPTYPE) {
// a heap type's tp_traverse already visited Py_TYPE(self)
} else {
if (Py_Version >= 0x03090000) {
Py_VISIT(Py_TYPE(self));
}
}
Обрабатывать счётчик ссылок типа в tp_new и tp_clear не требуется.
Определение tp_dealloc
Если у вашего типа есть собственная функция tp_dealloc, она должна:
- вызывать
PyObject_GC_UnTrack()до того, как какие-либо поля станут недействительными, и - уменьшать счётчик ссылок типа.
Чтобы тип оставался действительным во время вызова tp_free, счётчик ссылок типа необходимо уменьшать после освобождения экземпляра. Например:
static void my_dealloc(PyObject *self)
{
PyObject_GC_UnTrack(self);
...
PyTypeObject *type = Py_TYPE(self);
type->tp_free(self);
Py_DECREF(type);
}
Стандартная функция tp_dealloc делает это, поэтому, если ваш тип не переопределяет tp_dealloc, добавлять это не нужно.
Не переопределяйте tp_free
Слот tp_free типа в куче должен быть установлен в PyObject_GC_Del(). Это значение используется по умолчанию; не переопределяйте его.
Избегайте PyObject_New
Объекты, отслеживаемые сборщиком мусора, необходимо выделять с помощью функций, учитывающих сборку мусора.
Если вы используете PyObject_New() или PyObject_NewVar():
-
Если возможно, получите слот
tp_allocтипа и вызовите его. То есть заменитеTYPE *o = PyObject_New(TYPE, typeobj)на:TYPE *o = typeobj->tp_alloc(typeobj, 0);
Замените
o = PyObject_NewVar(TYPE, typeobj, size)на то же самое, но вместо 0 используйте размер. -
Если сделать это невозможно (например, внутри собственной
tp_alloc), вызовитеPyObject_GC_New()илиPyObject_GC_NewVar():TYPE *o = PyObject_GC_New(TYPE, typeobj); TYPE *o = PyObject_GC_NewVar(TYPE, typeobj, size);
Доступ к состоянию модуля из классов
Если у вас есть объект типа, определённый с помощью PyType_FromModuleAndSpec(), можно вызвать PyType_GetModule(), чтобы получить связанный с ним модуль, а затем вызвать PyModule_GetState(), чтобы получить состояние модуля.
Чтобы избежать утомительного шаблонного кода обработки ошибок, эти два шага можно объединить с помощью PyType_GetModuleState(). В результате получится:
my_struct *state = (my_struct*)PyType_GetModuleState(type);
if (state == NULL) {
return NULL;
}
Доступ к состоянию модуля из обычных методов
Получить доступ к состоянию уровня модуля из методов класса несколько сложнее, но это возможно благодаря API, появившемуся в Python 3.9. Чтобы получить состояние, сначала нужно получить определяющий класс, а затем — состояние модуля из него.
Основная сложность заключается в получении класса, в котором был определён метод, или, кратко, «определяющего класса» этого метода. Определяющий класс может ссылаться на модуль, частью которого он является.
Не путайте определяющий класс с Py_TYPE(self). Если метод вызван для подкласса вашего типа, Py_TYPE(self) будет ссылаться на этот подкласс, который может быть определён в модуле, отличном от вашего.
Примечание
Следующий код Python иллюстрирует эту концепцию. Base.get_defining_class возвращает Base, даже если type(self) == Sub:
class Base:
def get_type_of_self(self):
return type(self)
def get_defining_class(self):
return __class__
class Sub(Base):
pass
Чтобы метод мог получить свой «определяющий класс», он должен использовать METH_METHOD | METH_FASTCALL | METH_KEYWORDS в calling convention и соответствующую сигнатуру PyCMethod:
PyObject *PyCMethod(
PyObject *self, // object the method was called on
PyTypeObject *defining_class, // defining class
PyObject *const *args, // C array of arguments
Py_ssize_t nargs, // length of "args"
PyObject *kwnames) // NULL, or dict of keyword arguments
Получив определяющий класс, вызовите PyType_GetModuleState(), чтобы получить состояние связанного с ним модуля.
Например:
static PyObject *
example_method(PyObject *self,
PyTypeObject *defining_class,
PyObject *const *args,
Py_ssize_t nargs,
PyObject *kwnames)
{
my_struct *state = (my_struct*)PyType_GetModuleState(defining_class);
if (state == NULL) {
return NULL;
}
... // rest of logic
}
PyDoc_STRVAR(example_method_doc, "...");
static PyMethodDef my_methods[] = {
{"example_method",
(PyCFunction)(void(*)(void))example_method,
METH_METHOD|METH_FASTCALL|METH_KEYWORDS,
example_method_doc}
{NULL},
}
Доступ к состоянию модуля из методов-слотов, геттеров и сеттеров
Примечание
Нововведение Python 3.11.
Методы-слоты — быстрые эквиваленты специальных методов на C, например nb_add для __add__ или tp_new для инициализации, — имеют очень простой API, который не позволяет передать определяющий класс, в отличие от PyCMethod. То же относится к геттерам и сеттерам, определённым с помощью PyGetSetDef.
Чтобы получить доступ к состоянию модуля в таких случаях, воспользуйтесь функцией PyType_GetModuleByDef() и передайте ей определение модуля. Получив модуль, вызовите PyModule_GetState(), чтобы получить состояние:
PyObject *module = PyType_GetModuleByDef(Py_TYPE(self), &module_def);
my_struct *state = (my_struct*)PyModule_GetState(module);
if (state == NULL) {
return NULL;
}
Функция PyType_GetModuleByDef() выполняет поиск по порядку разрешения методов (то есть по всем суперклассам), чтобы найти первый суперкласс, связанный с соответствующим модулем.
Примечание
В крайне необычных случаях (цепочки наследования, охватывающие несколько модулей, созданных из одного определения) PyType_GetModuleByDef() может вернуть не модуль настоящего определяющего класса. Однако она всегда вернёт модуль с тем же определением, что гарантирует совместимую раскладку данных в памяти C.
Время жизни состояния модуля
Когда объект модуля удаляется сборщиком мусора, его состояние освобождается. Для каждого указателя на состояние модуля (или его часть) необходимо хранить ссылку на объект модуля.
Обычно это не проблема, поскольку типы, созданные с помощью PyType_FromModuleAndSpec(), и их экземпляры хранят ссылку на модуль. Однако при обращении к состоянию модуля из других мест, например из функций обратного вызова внешних библиотек, необходимо внимательно следить за подсчётом ссылок.
Нерешённые вопросы
Ряд вопросов, связанных с состоянием на уровне модуля и типами в куче, всё ещё остаётся нерешённым.
Обсуждать способы улучшения ситуации лучше всего на форуме discuss в разделе с тегом c-api.
Область видимости на уровне класса
В настоящее время (по состоянию на Python 3.11) нельзя связать состояние с отдельными типами, не полагаясь на особенности реализации CPython (которые могут измениться в будущем — возможно, как ни иронично, ради надлежащего решения для области видимости на уровне класса).
Преобразование типов в куче без потерь
API типов в куче не разрабатывался для преобразования статических типов «без потерь», то есть для создания типа, работающего в точности как заданный статический тип.
© 2001–2025 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.13/howto/isolating-extensions.html