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.