Контекстные менеджеры
Как 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— вход, после — выход.