Как написать свой контекстный менеджер с __enter__/__exit__ (а не просто with open)?
С with open(...) всё ясно, файл сам закрывается. А хочу сделать СВОЙ контекстный менеджер — например, замерять время выполнения блока или временно менять рабочую директорию. Понимаю, что нужны какие-то __enter__ и __exit__, но не соображу, как их правильно написать и что делать с исключениями внутри блока.
2 ответа
Есть два способа. Базовый — класс с методами __enter__ и __exit__. То, что возвращает __enter__, попадает в переменную после as; __exit__ вызывается на выходе из блока ВСЕГДА, даже если внутри вылетело исключение.
import time
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self # это уйдёт в 'as t'
def __exit__(self, exc_type, exc_val, exc_tb):
self.elapsed = time.perf_counter() - self.start
print(f"заняло {self.elapsed:.3f} c")
return False # False -> исключение НЕ глушим, пробросится дальше
with Timer() as t:
sum(range(10_000_000))
Про __exit__: в него приходят три аргумента про исключение. Если внутри блока всё прошло гладко — они None. Если вылетело исключение — там тип/значение/traceback. Вернёшь из __exit__ True — исключение «проглотится», вернёшь False/None — пробросится наверх (обычно нужно именно это).
Второй способ, короче, — декоратор @contextmanager из contextlib. Пишешь обычную функцию-генератор: всё до yield — это __enter__, всё после — __exit__, а try/finally гарантирует уборку даже при ошибке:
from contextlib import contextmanager
import os
@contextmanager
def cd(path):
old = os.getcwd()
os.chdir(path) # вход
try:
yield # тело with выполняется здесь
finally:
os.chdir(old) # выход — вернёт директорию даже при исключении
with cd("/tmp"):
print(os.getcwd()) # /tmp
print(os.getcwd()) # вернулись обратно
Для простых случаев @contextmanager обычно компактнее и читаемее. Класс берут, когда у менеджера есть состояние/несколько методов или его удобнее переиспользовать как объект.
Подводный камень с @contextmanager: оборачивай yield в try/finally, иначе при исключении в теле with код «после yield» (твоя уборка) просто не выполнится. Именно finally делает контекстный менеджер надёжным — это его главный смысл, гарантированная очистка ресурса.
И маленькое: один объект-генераторного @contextmanager нельзя использовать в двух with подряд — он одноразовый. Вызывай функцию заново на каждый with.