PythonMiddleTechnical
Как with гарантирует освобождение ресурсов при исключении?
Блок with гарантирует вызов __exit__ даже при исключении через механизм try/finally. __exit__ получает тип, значение и трейсбек исключения и может его подавить, вернув True.
Как with гарантирует освобождение ресурсов при исключении
Оператор with реализован через скрытый блок try/finally. Метод __exit__ контекстного менеджера вызывается всегда — независимо от того, завершился ли блок нормально или выбросил исключение.
Внутренняя механика
# Компилятор разворачивает:
# with open("file.txt") as f:
# data = f.read()
# В примерно такой код:
_mgr = open("file.txt")
f = _mgr.__enter__()
_exc = None
try:
data = f.read() # тело блока
except BaseException as e:
_exc = e
# __exit__ получает сведения об исключении
if _mgr.__exit__(type(e), e, e.__traceback__):
pass # исключение подавлено (возврат True)
else:
raise # исключение перебрасывается
else:
_mgr.__exit__(None, None, None) # нормальный выход
Пример: менеджер с логированием исключений
import logging
from types import TracebackType
from typing import Type
class ManagedResource:
def __init__(self, name: str) -> None:
self.name = name
self._handle: object = None
def __enter__(self) -> "ManagedResource":
logging.info("Opening %s", self.name)
self._handle = object() # имитация открытия ресурса
return self
def __exit__(
self,
exc_type: Type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> bool:
logging.info("Closing %s", self.name)
self._handle = None # всегда освобождаем ресурс
if exc_type is ValueError:
logging.warning("Suppressing ValueError: %s", exc_val)
return True # подавляем ValueError
return False # все прочие исключения пробрасываем дальше
with ManagedResource("db-connection") as res:
raise ValueError("bad input") # будет подавлено
print("Продолжаем выполнение") # выведется
try:
with ManagedResource("db-connection") as res:
raise RuntimeError("fatal") # НЕ подавляется
except RuntimeError:
print("RuntimeError не подавлен, но ресурс закрыт")
contextmanager и обработка исключений
from contextlib import contextmanager
import psycopg2
@contextmanager
def transaction(conn: psycopg2.extensions.connection):
try:
yield conn
conn.commit() # только при успехе
except Exception:
conn.rollback() # откатываем при любой ошибке
raise # пробрасываем исключение дальше
finally:
# finally выполняется всегда — идеально для закрытия
pass # conn.close() если нужно
ExitStack для динамического числа ресурсов
from contextlib import ExitStack
files = ["a.txt", "b.txt", "c.txt"]
with ExitStack() as stack:
handles = [stack.enter_context(open(f)) for f in files]
# Все файлы закроются при выходе, даже при исключении
data = [h.read() for h in handles]
Подводные камни
- Если
__enter__выбросит исключение,__exit__не будет вызван — ресурс, захваченный до исключения, останется незакрытым. - Возврат
Trueиз__exit__подавляет исключение молча — логируйте это явно, иначе ошибки теряются. - В
@contextmanagerесли забытьraiseв блокеexcept, исключение подавляется — поведение отличается от обычного класса менеджера. - При вложенных
withкаждый менеджер получает только исключение, возникшее в его области — внешний менеджер не видит исключений из внутреннего, если они подавлены. BaseException(включаяKeyboardInterrupt,SystemExit) тоже передаётся в__exit__— не заглушайте его случайно.- В async-коде нужен
async withи__aenter__/__aexit__; синхронный протокол там не работает.
Common mistakes
- Считать with аналогом только finally без обработки исключений.
- Не знать параметры exit.
- Не упоминать ограничение при падении enter.
What the interviewer is testing
- Объясняет вызов exit при исключении.
- Понимает suppress semantics.
- Знает границы гарантии.