Как устроены контекстные менеджеры и оператор with? Как написать свой?
Короткий ответ
- with гарантирует освобождение ресурса даже при исключении
- Протокол: методы __enter__ и __exit__
- __exit__ получает тип, значение и traceback исключения
- Вернуть True из __exit__ — подавить исключение
- @contextlib.contextmanager превращает генератор в менеджер
- Асинхронный вариант — async with, __aenter__/__aexit__
Контекстный менеджер через __enter__/__exit__ гарантирует корректное освобождение ресурсов, а contextlib позволяет писать их в виде генератора.
Как сказать вслух
пример ответаКонтекстный менеджер — это объект с методами enter и exit, который оператор with использует, чтобы гарантированно освободить ресурс: файл, соединение, блокировку — даже если внутри блока произошло исключение. Свой менеджер проще всего написать через декоратор contextmanager из contextlib: код до yield — вход, после — выход, и его оборачивают в try/finally. В exit можно обработать исключение и даже подавить его, вернув True.
Подробный ответ
Основной ответ
Оператор with вызывает __enter__ при входе в блок и __exit__ при выходе — всегда, включая выход по исключению. __exit__(exc_type, exc_value, traceback) получает информацию об ошибке (или три None) и может подавить её, вернув истинное значение. Это делает with идиомой управления ресурсами: файлы, локи, транзакции БД, временная подмена настроек. Альтернатива классу — @contextlib.contextmanager над функцией-генератором с одним yield: код до yield выполняется на входе, после — на выходе; yield стоит оборачивать в try/finally, чтобы очистка сработала при исключении. Для asyncio есть async with и протокол __aenter__/__aexit__. contextlib.ExitStack позволяет управлять динамическим числом ресурсов.
Ключевые моменты
- Гарантия очистки. __exit__ вызывается при любом выходе из блока — это замена try/finally, собранная в переиспользуемый объект.
- Обработка исключений. __exit__ видит исключение и решает: пробросить (вернуть False/None) или подавить (вернуть True).
- contextlib.contextmanager. Генератор с одним yield — самый короткий способ: меньше шаблонного кода, чем у класса.
- ExitStack и вложенность. with открывает несколько ресурсов в одной строке, а ExitStack — когда их число известно только в рантайме.
Практический контекст
В реальном коде это open(), lock в threading, transaction.atomic() в Django, сессии requests и httpx, pytest.raises в тестах. На собеседовании просят написать менеджер-таймер или транзакцию и проверяют, знает ли кандидат сигнатуру __exit__ и семантику возвращаемого значения. Упоминание async with и ExitStack выделяет кандидата уровня middle+.
Пример кода
from contextlib import contextmanager
import time
@contextmanager
def timer(label):
start = time.perf_counter()
try:
yield
finally:
print(f"{label}: {time.perf_counter() - start:.3f}s")
with timer("query"):
sum(range(10**6))Частые ошибки
- Не знают, что __exit__ вызывается и при исключении, и считают with просто «синтаксисом для файлов»
- В генераторной версии забывают try/finally вокруг yield — очистка теряется при ошибке
- Случайно подавляют исключения, вернув True из __exit__ без умысла