Представьте, что исключение перехватывается внутри тела with. Какие значения получит __exit__ в этом случае?
class Logger:
def __enter__(self):
print("enter")
return self
def __exit__(self, exc_type, exc, tb):
print(exc_type, exc)
return False
with Logger():
try:
raise ValueError("ошибка")
except ValueError:
print("handled")
__exit__ получит exc_type=None, exc=None и tb=None. Исключение было обработано внутри тела with, поэтому при выходе из него активного исключения уже нет.
__exit__ вызывается, но его результат в этой ситуации не влияет на обработанное исключение: возвращать True нечему.
Протокол контекстного менеджера появился вместе с конструкцией with, стандартизированной в Python 2.5 на основе PEP 343. Его исходная задача — гарантировать выполнение завершающих действий при обычном выходе из блока, исключении, return или break.
Для этого Python передаёт __exit__ информацию об исключении, которое покидает тело блока. Важна именно граница выхода из тела: исключение, полностью перехваченное раньше, до __exit__ уже не доходит.
Разработчик может ожидать, что контекстный менеджер всегда узнает о любой ошибке, произошедшей внутри его блока. Это неверно: внутренний try/except способен поглотить исключение, после чего контекстный менеджер увидит обычное завершение.
Такой нюанс критичен для транзакций, блокировок и аудита. Например, если __exit__ выполняет откат только при ненулевом exc_type, поглощённая ошибка может привести к фиксации частично выполненной операции.
При выполнении with Logger(): Python сначала вызывает __enter__, затем исполняет тело блока. Исключение ValueError возникает внутри вложенного try, но обработчик except перехватывает его и выполнение продолжается после обработчика.
К моменту выхода из тела with исключение уже не является распространяющимся наружу. Поэтому логически выполняется эквивалент обычного выхода:
Выводом будет None None. Возвращаемое значение __exit__ проверяется для исключения, которое ещё передаётся на выходе из тела: True подавляет его, False или None позволяет ему распространяться дальше. В рассмотренном случае подавлять уже нечего.
Если контекстному менеджеру нужно знать о внутренних сбоях, код должен явно сообщить об этом: повторно выбросить исключение, установить флаг ошибки или вызвать специальный метод. Однако ручной флаг легко забыть обновить, поэтому для транзакционной логики обычно надёжнее не перехватывать исключение без необходимости.
Отдельное ограничение: если исключение возникло в __enter__, __exit__ этого экземпляра не вызывается, поскольку вход в контекст не был успешно завершён.
В обработчике пакетной операции отдельная запись может завершаться ошибкой, но процесс хочет продолжить обработку остальных записей:
Здесь будет выбран commit, хотя запись завершилась ошибкой. Вариант с повторным raise корректно вызывает откат, но прерывает всю операцию; вариант с флагом позволяет продолжить обработку, однако требует явно связать этот флаг с решением о фиксации.
Практичное решение — разделить транзакционные границы: использовать отдельный with для каждой записи либо передавать менеджеру явный результат пакетной операции. Это не маскирует ошибку под успешное завершение и делает политику фиксации очевидной.
__exit__, если исключение перехвачено во внешнем try вокруг with?Да. Если исключение выходит из тела with, Python сначала вызывает __exit__, а уже затем внешний try/except может его перехватить. В этом случае __exit__ получит тип, объект исключения и traceback, даже если внешний обработчик впоследствии подавит ошибку.
__exit__ сам выбросит исключение после внутреннего except?Новое исключение покинет конструкцию with, несмотря на то что исходная ошибка была обработана внутри тела. Для внешнего кода причиной будет исключение из __exit__; исходная ошибка может сохраняться в цепочке исключений как контекст, если Python установит его автоматически.
__exit__ сведения об исключении, если внутри блока выполнен return?Нет, при обычном return передаются None, None, None. __exit__ всё равно будет вызван до фактического возврата из функции, но значение return не считается исключением и не может быть подавлено возвращаемым значением __exit__.