Контекстные менеджеры

Как with гарантирует освобождение ресурсов и как написать свой менеджер двумя способами.

Контекстный менеджер — объект, который определяет, что сделать при входе в блок with и при выходе из него (в том числе при ошибке).

Зачем нужен with

Многие ресурсы нужно обязательно освобождать: закрыть файл, отпустить блокировку, закрыть соединение. Если делать это вручную, легко забыть — особенно когда внутри возникает исключение. Оператор with гарантирует, что код «выхода» выполнится всегда, даже при ошибке.

Протокол __enter__ / __exit__

Чтобы объект работал в with, у него должны быть два метода: __enter__ (вызывается при входе, его результат попадает в переменную после as) и __exit__ (вызывается при выходе — нормальном или из-за исключения).

class Managed:
    def __enter__(self):
        print("вход: ресурс захвачен")
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("выход: ресурс освобождён")
        return False   # не подавляем исключения

with Managed() as m:
    print("работаем внутри блока")

print("---")

try:
    with Managed() as m:
        print("сейчас будет ошибка")
        raise ValueError("что-то сломалось")
except ValueError as e:
    print("поймали:", e)

Вывод:

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

Главное наблюдение: «выход: ресурс освобождён» печатается в обоих случаях — и при нормальном завершении, и при исключении. Аргументы exc_type, exc_val, exc_tb в __exit__ описывают исключение (или равны None, если ошибки не было). Возврат False означает «не глотать исключение» — оно пробросится дальше.

contextlib.contextmanager: короче

Писать класс с двумя методами ради простого менеджера громоздко. Декоратор contextlib.contextmanager позволяет описать менеджер генератором: всё до yield — это «вход», всё после — «выход».

from contextlib import contextmanager

@contextmanager
def tag(name):
    print(f"<{name}>")     # вход
    yield                       # здесь выполняется тело with
    print(f"</{name}>")    # выход

with tag("b"):
    print("жирный текст")

with tag("i"):
    print("курсив")

Вывод:

<b>
жирный текст
</b>
<i>
курсив
</i>

Код до yield отрабатывает при входе в блок, само yield «отдаёт управление» телу with, а строки после yield — при выходе. Это компактная замена паре __enter__/__exit__ для несложных случаев.

Когда что выбирать

  • Класс с __enter__/__exit__ — когда менеджер хранит состояние или нужна тонкая обработка исключений в __exit__.
  • @contextmanager — для коротких менеджеров вида «сделать до / сделать после».

Итог

  • with гарантирует выполнение «выхода» даже при исключении.
  • Протокол менеджера — методы __enter__ (результат идёт в as) и __exit__ (вызывается всегда).
  • @contextlib.contextmanager описывает менеджер генератором: до yield — вход, после — выход.
Проверьте себя
1. Что гарантирует оператор with?
AЧто код внутри выполнится быстрее
BЧто метод __exit__ (освобождение ресурса) выполнится даже при исключении
CЧто исключения не возникнут
DЧто ресурс никогда не освободится
2. Куда попадает результат метода __enter__?
AВ переменную после ключевого слова as
BВ метод __exit__
CВ глобальную область видимости
DНикуда, он игнорируется
3. Как работает менеджер, созданный через @contextmanager?
AКод до yield — вход, тело with выполняется на yield, код после yield — выход
BВесь код выполняется при выходе
Cyield запускает менеджер в отдельном потоке
DОн не поддерживает исключения вовсе
Поддержать проект