← Назад к списку
ТехническаяPythonMiddle

Как устроены контекстные менеджеры и оператор 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__ без умысла

ИП Кочкин Алексей Сергеевич · ИНН 390509026279 · ОГРНИП 325390000030973 · jiniys2005@yandex.ru