Контекстные менеджеры(Context managers)

Контекстные менеджеры(Context managers)

KarmanovichDev

В PEP 343(The “with” Statement) появился оператор with, для возможности исключения стандартного использования try/finally.

Придётся дублировать этот код для корректной работы с файлами. Также, если логика захвата и освобождения ресурсов будет сложна, это будет явным нарушением DRY и усложнением вашей жизни.

Протокол контекстных менеджеров состоит из двух методов __enter__, __exit__

В __enter__ мы должны "захватить" некий ресурс выполнения и "удерживать" его в рамках выполнения контекстного менеджера. При желании можно вернуть возможность управления этим ресурсом клиентскому коду:

  • Соединение с базой данных(сессии, транзакции)
  • Авторизация в стороннем сервисе(все запросы в рамках контекстного менеджера будут авторизованны)
  • Работа с файлами(builtins.open())

__exit__ Вызывается всегда, если __enter__ завершился без ошибок. В __exit__ мы должны "освободить"/закрыть ранее захваченный ресурс. И обработать ошибки если они возникли в рамках выполнения менеджера.

В __exit__ приходит три аргумента:

  • exp_type - Класс исключения, которое произошло в рамках выполнения контекстного менеджера
  • exp_value - Экземпляр класса exp_type
  • exc_tb - Объект трассировки(traceback)

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

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


Вот так, могла бы выглядеть обёртка над open, если бы сам open не имел контекстного менеджера, для контроля за контекстом выполнения.

Для асинхронного менеджера контекста потребуются __aenter__ и __aexit__

Обратите внимание, мы ничего не возвращаем и __exit__. Неявно python вернёт за нас None, который в свою очередь является ложным объектом bool(None) == False. Из этого следует, что если в рамках работы контекстного менеджера было возбуждено исключение, то оно будет возбуждено сразу после закрытия соединения с файлом автоматически(без нашего участия).


Также стоит понимать, что если исключение возникнет внутри метода __exit__ , то именно оно будет в конце итогового объекта трассировки.


Также, есть способ создать контекстные менеджеры с использованием try/finally. Это декоратор contextlib.contextmanager/asynccontextmanager:

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


Вот несколько контекстных менеджеров, которые я встречался сам:

  1. open - для работы с файлами
  2. atomic из django.db.transaction - позволяет создать блок кода, в котором гарантируется атомарность(Acid) базы данных
  3. pytest.raises - Проверка, что в рамках кода было вызвано ожидаемое исключение
  4. contextlib.nullcontext - контекстный менеджер, который не делает ничего(да и такие бывают нужны)
  5. tempfile.TemporaryDirectory/File - создание временной директории/файла
  6. multiprocessing.Lock - работа с многопроцессовыми блокировками
  7. threading.Lock - работа с многопоточными блокировками

На самом деле их значительно больше, даже в стандартной библиотеке python.


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

Report Page