PythonJuniorLive coding

Как написать свой context manager через класс?

Опишите класс с методами __enter__ (возвращает ресурс) и __exit__(exc_type, exc, tb) (освобождает ресурс). __exit__ возвращает False/None, чтобы исключение пробросилось дальше; True подавляет.

Протокол

Context manager — это объект с двумя дандер-методами:

  • __enter__(self) — вызывается на входе в with. Возвращаемое значение биндится к переменной после as.
  • __exit__(self, exc_type, exc, tb) — вызывается всегда: и на нормальном выходе, и при исключении. Если возвращает truthy (True), исключение подавляется; False/None — пробрасывается дальше.

Если ресурс уже создан и нужно только распарсить enter/exit — короче через @contextlib.contextmanager. Класс предпочтительнее, когда у ресурса есть состояние, несколько методов или сложный lifecycle (например, сессия БД, файловый lock, span трассировки).

Пример: таймер с замером времени

from time import perf_counter


class Timer:
    def __init__(self, label: str = "block"):
        self.label = label
        self.elapsed: float = 0.0

    def __enter__(self) -> "Timer":
        self._start = perf_counter()
        return self

    def __exit__(self, exc_type, exc, tb) -> bool:
        self.elapsed = perf_counter() - self._start
        print(f"[{self.label}] took {self.elapsed:.4f}s "
              f"(exc={exc_type.__name__ if exc_type else None})")
        return False  # не подавляем исключение


with Timer("hash") as t:
    sum(i * i for i in range(1_000_000))

print(t.elapsed)

Пример: транзакция с rollback при ошибке

class Transaction:
    def __init__(self, conn):
        self.conn = conn

    def __enter__(self):
        self.conn.execute("BEGIN")
        return self.conn

    def __exit__(self, exc_type, exc, tb):
        if exc_type is None:
            self.conn.execute("COMMIT")
        else:
            self.conn.execute("ROLLBACK")
        return False  # пробрасываем ошибку наружу

Async-вариант

Для async-ресурсов реализуйте __aenter__ и __aexit__ и используйте async with. Сигнатуры те же, но методы — корутины.

Подводные камни

  • Случайно вернуть truthy из __exit__ (например, забыть return False и вернуть результат логирования) — исключения молча проглатываются.
  • Освобождение в __exit__ само бросает исключение — оно перекроет оригинальное; оборачивайте cleanup в try/except или используйте contextlib.ExitStack.
  • Ошибка внутри __enter__ после частичного захвата ресурса: __exit__ не вызывается, нужен ручной cleanup в except.
  • Делать __enter__ неидемпотентным и пытаться вложить with одного и того же экземпляра — состояние перезапишется.
  • Использовать класс там, где хватило бы @contextlib.contextmanager + generator — лишний boilerplate.

Common mistakes

  • Реализовать только enter.
  • Писать cleanup после блока вместо exit.
  • Возвращать True без причины.

What the interviewer is testing

  • Код запускается через with.
  • Исключения не проглатываются.
  • Cleanup находится в exit.

Sources

Related topics