Документация Python неофициальный перевод
Содержание страницы

Изоляция модулей расширенийIsolating Extension Modules

Кому следует это прочитатьWho should read this

Это руководство предназначено для мейнтейнеров расширений C-API, которые хотят сделать это расширение более безопасным в приложениях, где сам Python используется как библиотека.

ПредысторияBackground

Интерпретатор – это среда выполнения кода Python. Он содержит конфигурацию (например, путь импорта) и состояние времени выполнения (например, набор импортированных модулей).

Python поддерживает запуск нескольких интерпретаторов в одном процессе. Рассмотрим два случая: пользователи могут запускать интерпретаторы

Оба случая (и их комбинации) наиболее полезны при встраивании Python в библиотеку. Библиотеки обычно не должны строить предположения о приложении, которое их использует, в том числе предполагать наличие «главного интерпретатора Python» в рамках процесса.

Исторически сложилось, что расширения Python плохо обрабатывают этот вариант использования. Многие расширения (и даже некоторые модули из stdlib) используют глобальное состояние процесса, потому что C-переменные static чрезвычайно просты в использовании. Таким образом, данные, которые должны быть специфичны для конкретного интерпретатора, в итоге разделяются между интерпретаторами. Если разработчик расширения не будет внимателен, очень легко ввести краевые случаи, ведущие к аварийным завершениям, когда модуль загружается в нескольких интерпретаторах одного процесса.

К сожалению, состояние на интерпретатор достичь непросто. Авторы расширений обычно не думают о нескольких интерпретаторах при разработке, и в настоящее время тестировать такое поведение сложно.

Состояние модуляEnter Per-Module State

Вместо концентрации на состоянии на интерпретатор, C API Python развивается в сторону лучшей поддержки более детализированного состояния на модуль. Это означает, что данные уровня C должны быть привязаны к объекту модуля. Каждый интерпретатор создаёт свой собственный объект модуля, так что данные остаются разделёнными. Для проверки изоляции в одном интерпретаторе можно загружать даже несколько объектов модулей, соответствующих одному расширению.

Состояние на модуль даёт простой способ мыслить о времени жизни и владении ресурсами: расширение инициализируется при создании объекта модуля и очищается при его освобождении. В этом отношении модуль – это просто любой другой PyObject*; не нужно думать – или забывать – о хуках «при завершении интерпретатора».

Обратите внимание, что существуют варианты использования разных видов «глобальных» переменных: состояние на процесс, на интерпретатор, на поток или на задачу. При состоянии на модуль как умолчании они всё ещё возможны, но их следует рассматривать как исключительные случаи: если они нужны, нужно уделить им дополнительное внимание и тестирование. (Заметим, что данное руководство их не рассматривает.)

Изолированные объекты модулейIsolated Module Objects

Ключевой момент, который следует помнить при разработке расширения: из одной разделяемой библиотеки может быть создано несколько объектов модуля. Например:

>>> import sys
>>> import binascii
>>> old_binascii = binascii
>>> del sys.modules['binascii']
>>> import binascii  # создать новый объект модуля
>>> old_binascii == binascii
False

Как правило, два модуля должны быть полностью независимы. Все объекты и состояние, специфичные для модуля, должны быть инкапсулированы внутри объекта модуля, не разделяться с другими объектами модулей и очищаться при освобождении объекта модуля. Поскольку это лишь общее правило, возможны исключения (см. раздел Управление глобальным состоянием), но они потребуют больше размышлений и внимания к краевым случаям.

Хотя некоторые модули могли бы обойтись менее строгими ограничениями, изолированные модули упрощают установление чётких ожиданий и правил, работающих для множества вариантов использования.

Неожиданные краевые случаиSurprising Edge Cases

Заметьте, что изолированные модули действительно создают некоторые неожиданные краевые случаи. В частности, каждый объект модуля обычно не разделяет свои классы и исключения с другими аналогичными модулями. Продолжая пример выше, заметьте, что 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 «вручную» считается хаком.

Обеспечение безопасности модулей при нескольких интерпретаторахMaking Modules Safe with Multiple Interpreters

Управление глобальным состояниемManaging Global State

Иногда состояние, связанное с модулем Python, не является специфичным для этого модуля, а относится ко всему процессу (или чему-то «более глобальному», чем модуль). Например:

  • Модуль readline управляет терминалом.

  • Модуль, работающий на плате, хочет управлять встроенным светодиодом.

В таких случаях модуль Python должен предоставлять доступ к глобальному состоянию, а не владеть им. Если возможно, напишите модуль так, чтобы несколько его копий могли обращаться к состоянию независимо (наряду с другими библиотеками, будь то для Python или других языков). Если это невозможно, рассмотрите явную блокировку.

Если необходимо использовать глобальное состояние процесса, самый простой способ избежать проблем с несколькими интерпретаторами – явно запретить загрузку модуля более одного раза на процесс – см. Отказ: Ограничение одним объектом модуля на процесс.

Управление состоянием на модульManaging Per-Module State

Чтобы использовать состояние на модуль, используйте многофазную инициализацию расширения. Это сигнализирует, что ваш модуль корректно поддерживает несколько интерпретаторов.

Установите PyModuleDef.m_size в положительное число, чтобы запросить соответствующее количество байт локального хранилища для модуля. Обычно это устанавливается в размер некоторой struct, специфичной для модуля, которая может хранить всё C-уровневое состояние модуля. В частности, сюда следует помещать указатели на классы (включая исключения, но исключая статические типы) и настройки (например, csv’s field_size_limit), которые нужны C-коду для работы.

Примечание

Другой вариант – хранить состояние в __dict__ модуля, но при этом нужно избегать аварийного завершения, когда пользователи изменяют __dict__ из кода Python. Обычно это означает проверку ошибок и типов на уровне C, что легко сделать неправильно и сложно протестировать в достаточной мере.

Однако, если состояние модуля не нужно в C-коде, хранение его только в __dict__ – хорошая идея.

Если состояние модуля включает указатели PyObject, объект модуля должен удерживать ссылки на эти объекты и реализовывать хуки на уровне модуля m_traverse, m_clear и m_free. Они работают как tp_traverse, tp_clear и tp_free у класса. Их добавление потребует некоторой работы и сделает код длиннее; это цена за модули, которые можно чисто выгружать.

Пример модуля с состоянием уровня модуля в настоящее время доступен как xxlimited; пример инициализации модуля приведён в нижней части файла.

Отказ: ограничение одним объектом модуля на процессOpt-Out: Limiting to One Module Object per Process

Неотрицательное значение PyModuleDef.m_size указывает, что модуль корректно поддерживает несколько интерпретаторов. Если ваш модуль этого ещё не делает, вы можете явно сделать его загружаемым только один раз на процесс. Например:

// Флаг уровня процесса
static int loaded = 0;

// Мьютекс для обеспечения потокобезопасности (требуется только для 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);
    // остальная часть инициализации
}

Если функция PyModuleDef.m_clear вашего модуля способна подготовиться к будущей повторной инициализации, она должна сбросить флаг loaded. В этом случае ваш модуль не будет поддерживать одновременное существование нескольких экземпляров одновременно, но он будет, например, поддерживать загрузку после завершения работы среды выполнения Python (Py_FinalizeEx()) и повторную инициализацию (Py_Initialize()).

Доступ к состоянию модуля из функцийModule State Access from Functions

Доступ к состоянию из функций уровня модуля не представляет сложности. Функции получают объект модуля в качестве первого аргумента; для извлечения состояния можно использовать PyModule_GetState:

static PyObject *
func(PyObject *module, PyObject *args)
{
    my_struct *state = (my_struct*)PyModule_GetState(module);
    if (state == NULL) {
        return NULL;
    }
    // остальная часть логики
}

Примечание

PyModule_GetState может вернуть NULL без установки исключения, если состояния модуля нет, т.е. PyModuleDef.m_size был нулевым. В вашем модуле управление m_size принадлежит вам, так что это легко предотвратить.

Кучные типыHeap Types

Традиционно типы, определённые в коде на C, являются статическими; то есть это структуры static PyTypeObject, определяемые непосредственно в коде и инициализируемые с помощью PyType_Ready().

Такие типы обязательно являются общими для всего процесса. Совместное использование их между объектами модулей требует внимания к любому состоянию, которым они владеют или к которому обращаются. Чтобы ограничить возможные проблемы, статические типы неизменяемы на уровне Python: например, нельзя задать str.myattribute = 123.

Особенность реализации CPython: Совместное использование по-настоящему неизменяемых объектов между интерпретаторами не вызывает проблем, пока они не предоставляют доступа к изменяемым объектам. Однако в CPython каждый объект Python содержит изменяемую деталь реализации: счётчик ссылок. Изменения счётчика ссылок защищены GIL. Таким образом, код, разделяющий любые объекты Python между интерпретаторами, неявно зависит от текущего, общепроцессного GIL в CPython.

Поскольку статические типы неизменяемы и глобальны для процесса, они не могут получить доступ к «своему» состоянию модуля. Если какой-либо метод такого типа требует доступа к состоянию модуля, тип необходимо преобразовать в тип, размещаемый в куче, или, сокращённо, кучный тип. Они больше соответствуют классам, создаваемым оператором class в Python.

Для новых модулей использование кучных типов по умолчанию является хорошим эмпирическим правилом.

Преобразование статических типов в кучные типыChanging Static Types to Heap Types

Статические типы можно преобразовать в кучные, но учтите, что API кучных типов не был разработан для преобразования «без потерь» из статических типов – то есть для создания типа, работающего в точности как данный статический тип. Поэтому при переписывании определения класса в новом API вы, вероятно, непреднамеренно измените некоторые детали (например, возможность сериализации или унаследованные слоты). Всегда тестируйте детали, важные для вас.

Обратите особое внимание на следующие два пункта (но учтите, что это не исчерпывающий список):

  • В отличие от статических типов, объекты кучных типов по умолчанию изменяемы. Используйте флаг Py_TPFLAGS_IMMUTABLETYPE, чтобы предотвратить изменяемость.

  • Кучные типы по умолчанию наследуют tp_new, поэтому может стать возможным создание их экземпляров из кода Python. Вы можете предотвратить это с помощью флага Py_TPFLAGS_DISALLOW_INSTANTIATION.

Определение кучных типовDefining Heap Types

Кучные типы можно создать, заполнив структуру PyType_Spec, которая является описанием или «чертежом» класса, и вызвав PyType_FromModuleAndSpec() для создания нового объекта класса.

Примечание

Другие функции, такие как PyType_FromSpec(), также могут создавать кучные типы, но PyType_FromModuleAndSpec() связывает модуль с классом, предоставляя доступ к состоянию модуля из методов.

Класс обычно следует хранить как в состоянии модуля (для безопасного доступа из C), так и в __dict__ модуля (для доступа из кода Python).

Протокол сборки мусораGarbage-Collection Protocol

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

Чтобы избежать утечек памяти, экземпляры кучных типов должны реализовывать протокол сборки мусора. А именно, кучные типы должны:

  • Иметь флаг 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 in Python 3.8 and lower

Требование посещать тип из 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. В качестве замены используйте:

Делегирование tp_traverseDelegating tp_traverse

Если функция обхода делегирует tp_traverse своего базового класса (или другого типа), убедитесь, что Py_TYPE(self) посещается только один раз. Обратите внимание, что только тип кучи должен посещать тип в tp_traverse.

Например, если ваша функция обхода включает:

base->tp_traverse(self, visit, arg)

…и base может быть статическим типом, тогда она также должна включать:

if (base->tp_flags & Py_TPFLAGS_HEAPTYPE) {
    // tp_traverse типа‑кучи уже посетил Py_TYPE(self)
} else {
    if (Py_Version >= 0x03090000) {
        Py_VISIT(Py_TYPE(self));
    }
}

Нет необходимости обрабатывать счетчик ссылок типа в tp_new и tp_clear.

Определение tp_deallocDefining 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_freeNot overriding tp_free

Слот tp_free типа кучи должен быть установлен в PyObject_GC_Del(). Это значение по умолчанию; не переопределяйте его.

Избегание PyObject_NewAvoiding 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) на то же самое, но использовать 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);
    

Доступ к состоянию модуля из классовModule State Access from Classes

Если у вас есть объект типа, определенный с PyType_FromModuleAndSpec(), вы можете вызвать PyType_GetModule(), чтобы получить связанный модуль, а затем PyModule_GetState(), чтобы получить состояние модуля.

Чтобы избавиться от утомительного шаблонного кода обработки ошибок, вы можете объединить эти два шага с помощью PyType_GetModuleState(), получив:

my_struct *state = (my_struct*)PyType_GetModuleState(type);
if (state == NULL) {
    return NULL;
}

Доступ к состоянию модуля из обычных методовModule State Access from Regular Methods

Доступ к состоянию на уровне модуля из методов класса несколько сложнее, но возможен благодаря 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,               // объект, на котором вызван метод
    PyTypeObject *defining_class, // определяющий класс
    PyObject *const *args,        // C‑массив аргументов
    Py_ssize_t nargs,             // длина "args"
    PyObject *kwnames)            // NULL или словарь именованных аргументов

Получив определяющий класс, вызовите 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;
    }
    ... // остальная логика
}

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

Доступ к состоянию модуля из слотовых методов, геттеров и сеттеровModule State Access from Slot Methods, Getters and Setters

Примечание

Это новое в 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.

Время жизни состояния модуляLifetime of the Module State

Когда объект модуля собирается сборщиком мусора, его состояние освобождается. Для каждого указателя на (часть) состояния модуля необходимо удерживать ссылку на объект модуля.

Обычно это не проблема, потому что типы, созданные с помощью PyType_FromModuleAndSpec(), и их экземпляры хранят ссылку на модуль. Однако нужно быть осторожным с подсчётом ссылок, когда вы ссылаетесь на состояние модуля из других мест, например, из колбэков внешних библиотек.

Открытые вопросыOpen Issues

Несколько вопросов, связанных с состоянием на модуль и кучевыми типами, всё ещё открыты.

Обсуждения по улучшению ситуации лучше всего вести на форуме discuss под тегом c-api.

Область видимости для классаPer-Class Scope

На данный момент (начиная с Python 3.11) невозможно привязать состояние к отдельным типам, не полагаясь на детали реализации CPython (которые могут измениться в будущем – возможно, по иронии судьбы, чтобы обеспечить правильное решение для области видимости на класс).

Преобразование в кучевые типы без потерьLossless Conversion to Heap Types

API кучевых типов не был разработан для «без потерь» преобразования из статических типов; то есть для создания типа, который работает точно так же, как заданный статический тип.