← Сортировка · 🏠 Домой · match / case →
Коротко. Объект с методами __enter__ и __exit__. with вызывает
первый на входе в блок, второй — на выходе, при любом исходе: обычном
завершении, return, break или исключении.
class Ctx:
def __enter__(self):
print("enter")
return self # это и попадёт в "as"
def __exit__(self, exc_type, exc, tb):
print("exit", exc_type)
return False # исключение не подавляем
with Ctx() as c:
raise ValueError("boom")
# enter
# exit <class 'ValueError'>
# ValueError: boom — вылетело наружу__exit__ получает три аргумента — тип, значение и traceback исключения,
либо три None, если блок отработал без ошибки.
flowchart TB
w["with Ctx() as c:"] --> en["__enter__()<br/>возвращаемое значение попадает в as"]
en --> body["тело блока"]
body -->|"нормальное завершение,<br/>return или break"| ex1["__exit__(None, None, None)"]
body -->|"исключение"| ex2["__exit__(тип, значение, traceback)"]
ex2 --> ret{"что вернул __exit__?"}
ret -->|"истинное значение"| supp["исключение подавлено,<br/>выполнение идёт дальше"]
ret -->|"False или None"| prop["исключение летит наружу"]
ex1 --> after["код после блока"]
Подвох. as связывает то, что вернул __enter__, а не сам менеджер.
Классический пример: with lock: без as, потому что Lock.__enter__
возвращает True, а не сам объект блокировки.
Коротко. Вернув из __exit__ истинное значение. Тогда исключение
считается обработанным и не распространяется дальше.
class Swallow:
def __enter__(self): return self
def __exit__(self, *exc_info):
return True # проглотили
with Swallow():
raise ValueError("x")
print("alive")
# aliveГотовая реализация этого — contextlib.suppress, замена «пустому except»:
from contextlib import suppress
with suppress(FileNotFoundError):
open("nope.txt")Подвох. Молчаливое возвращение True из __exit__ — источник тяжело
находимых багов: менеджер, написанный «для логирования», начинает глотать
все ошибки блока. Если явного return нет, функция вернёт None — ложное
значение, и это правильное поведение по умолчанию.
Коротко. Ничем по семантике — with и есть try/finally, но с логикой
захвата и освобождения, вынесенной в один переиспользуемый объект, а не
продублированной на каждом вызове.
# эквивалент with open(...) as f:
f = open("data.txt")
try:
data = f.read()
finally:
f.close()Несколько менеджеров в одном with — это вложенность, они закрываются
в обратном порядке:
with open("in.txt") as src, open("out.txt", "w") as dst:
dst.write(src.read())Глубже. finally выполняется даже при return внутри try: значение уже
вычислено, но управление сначала пройдёт через finally. А return внутри
самого finally перетрёт и результат, и летящее исключение — поэтому так
никогда не пишут (с Python 3.14 это ещё и предупреждение компилятора,
PEP 765).
Коротко. Декоратором @contextlib.contextmanager над генератором с одним
yield: всё до yield — это __enter__, всё после — __exit__, а сам
yield отдаёт значение в as.
from contextlib import contextmanager
@contextmanager
def tag(name):
print(f"<{name}>")
try:
yield name
finally:
print(f"</{name}>")
with tag("b") as t:
print(t)
# <b>
# b
# </b>Подвох. try/finally вокруг yield обязателен. Без него исключение из
тела with пробрасывается в генератор прямо в точку yield, код после него
не выполнится — и ресурс не освободится.
Глубже. Полезное из contextlib:
closing(obj)— вызватьobj.close()на выходе для объектов без протокола;ExitStack()— динамическое количество менеджеров (список файлов, длина которого известна только в рантайме),stack.enter_context(cm);nullcontext(value)— «пустой» менеджер, чтобы не ветвить код наwith/безwith;@asynccontextmanagerиasync with— те же протоколы__aenter__и__aexit__для asyncio (см.asyncio.TaskGroup, который сам является асинхронным контекстным менеджером).
Класс, реализующий протокол, к тому же переиспользуем: экземпляр
@contextmanager-генератора одноразовый, войти в него дважды нельзя.