PythonMiddleTechnical
Как работают __enter__ и __exit__?
__enter__(self) вызывается при входе в with и возвращает значение для as. __exit__(self, exc_type, exc, tb) вызывается при выходе всегда (даже при исключении). Возврат True подавляет исключение, False/None — пропускает дальше. Если __enter__ упал — __exit__ не вызывается.
Сигнатуры
class CM:
def __enter__(self): # без параметров (кроме self)
...
return value # уходит в "as var"
def __exit__(self, exc_type, exc, tb): # ровно три параметра exc-info
...
return suppress # bool: подавить ли исключение
Что делает with под капотом
Конструкция with expr as var: BLOCK разворачивается примерно так:
mgr = expr
value = type(mgr).__enter__(mgr)
exc = True
try:
try:
var = value
BLOCK
except:
exc = False
if not type(mgr).__exit__(mgr, *sys.exc_info()):
raise
finally:
if exc:
type(mgr).__exit__(mgr, None, None, None)
Важные следствия:
__exit__зовётся всегда — и при успехе (с тройкой None), и при исключении (с exc_type/exc/tb).- Если
__enter__сам бросил исключение —__exit__не вызывается, потому что менеджер ещё не «вошёл». - Truthy-возврат
__exit__подавляет исключение: код послеwithпродолжит выполнение. - Методы ищутся на классе, не на инстансе:
cm.__enter__ = lambda s: ...не сработает.
Полноценный пример
import time
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self # доступ к self.elapsed после with
def __exit__(self, exc_type, exc, tb):
self.elapsed = time.perf_counter() - self.start
if exc_type is not None:
print(f"failed after {self.elapsed:.3f}s: {exc!r}")
# вернём None — не подавляем
with Timer() as t:
sum(range(1_000_000))
print(f"{t.elapsed:.3f}s")
Подавление исключения
class Suppress:
def __init__(self, *exceptions):
self.exceptions = exceptions
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return exc_type is not None and issubclass(exc_type, self.exceptions)
with Suppress(FileNotFoundError):
open("nope.txt")
print("alive") # выполнится
В stdlib это уже есть как contextlib.suppress.
Подводные камни
__exit__бросает своё исключение — оно заменит исходное (chained через__context__). Логируйте, не превращайте cleanup в новый источник ошибок.- Случайный
return Trueподавляет ошибки и маскирует баги. - Открытие ресурса в
__init__вместо__enter__— ресурс утечёт, если объект создан, ноwithне выполнен. - Многошаговый
__enter__без отката частично захваченного состояния — на середине прерывается, и предыдущие шаги остаются. - Использование sync
withдля async-менеджера: нуженasync withи__aenter__/__aexit__. - Параметр
tb— этоTracebackType, а не строка; для рендера нуженtraceback.format_exception.
Common mistakes
- Не знать сигнатуру exit.
- Думать, что exit вызывается только при успехе.
- Возвращать True без намерения подавить ошибку.
What the interviewer is testing
- Расписывает with как try/except/else.
- Объясняет параметры исключения.
- Понимает роль return value exit.