Вам передали контекстный менеджер. Определите, дойдёт ли ValueError до обработчика и что напечатает программа.
class CM:
def __enter__(self):
print("enter")
return self
def __exit__(self, exc_type, exc, tb):
print(exc_type.__name__)
return True
try:
with CM():
print("body")
raise ValueError("bad")
except ValueError:
print("propagated")
else:
print("handled")
ValueError не дойдёт до блока except, потому что метод __exit__ возвращает True и тем самым подавляет исключение. Программа напечатает:
Конструкция with и протокол контекстных менеджеров были введены для надёжного управления ресурсами: файлами, блокировками, транзакциями и временными изменениями состояния. Такой подход решает проблему гарантированного выполнения завершающих действий независимо от того, завершился блок нормально или возникло исключение.
Контекстный менеджер отделяет управление ресурсом от основной логики блока. Это уменьшает дублирование try/finally и делает границы жизненного цикла ресурса явными.
При входе в with нужно выполнить подготовку, а при выходе — освобождение ресурса. При исключении Python должен передать контекст ошибки методу __exit__, чтобы объект мог либо обработать её, либо позволить исключению распространяться дальше.
Неверное возвращаемое значение __exit__ может скрыть ошибку от вызывающего кода. Поэтому подавлять исключения следует только тогда, когда контекстный менеджер действительно обработал их и это соответствует его контракту.
Выражение with CM() сначала создаёт объект CM, затем вызывает его __enter__. Возвращаемое значение __enter__ присваивается переменной после as, если она указана. В примере результат возвращается, но не используется.
После этого выполняется тело блока и печатается body. Вызов raise ValueError(...) прерывает обычное выполнение тела, поэтому Python вызывает __exit__ примерно так:
Первый аргумент после self — класс исключения, второй — его экземпляр, третий — объект трассировки. Поэтому exc_type.__name__ даёт строку ValueError.
Если __exit__ возвращает истинное значение, исключение считается обработанным и не передаётся дальше. В данном случае возвращается True, поэтому блок except ValueError не выполняется, а управление переходит к ветке else конструкции try.
Если бы __exit__ вернул False или None, исключение продолжило бы распространяться, и был бы выполнен блок except. При нормальном завершении тела все три аргумента __exit__ равны None.
Метод __exit__ вызывается для уже успешно вошедшего контекстного менеджера. Если __enter__ сам завершится исключением, __exit__ этого же объекта автоматически не вызывается.
Предположим, контекстный менеджер управляет транзакцией базы данных. При нормальном выходе он делает commit, а при исключении — rollback. Вариант с безусловным return True прост, но опасен: ошибка запроса может исчезнуть, и приложение продолжит работу с неверными предположениями.
Вариант с безусловным return False безопаснее для диагностики: исключения не скрываются, но контекстный менеджер не может обработать ожидаемую ошибку на своей границе. Практичнее возвращать истинное значение только для явно предусмотренных исключений:
Такой вариант сохраняет исходную ошибку для неожиданных сбоев, но позволяет транзакционному слою обработать известную бизнес-ситуацию. На практике важно также не допускать, чтобы ошибка в rollback или commit маскировалась неявно: исключение из __exit__ заменит исходное исключение как непосредственную причину сбоя.
Что произойдёт, если __enter__ выбросит исключение?
__exit__ этого же контекстного менеджера вызван не будет, потому что вход в контекст не завершился успешно. Исключение из __enter__ распространяется наружу обычным образом, если его не перехватит внешний контекстный менеджер или обработчик.
Что произойдёт, если __exit__ сам выбросит исключение?
Исключение из __exit__ станет новым исключением, выходящим из конструкции with. Исходное исключение при этом обычно сохраняется в атрибуте __context__, но непосредственным результатом выполнения будет ошибка из __exit__. Поэтому код освобождения ресурса должен быть достаточно надёжным и не должен без необходимости заменять исходную ошибку.
Как работает подавление в нескольких вложенных контекстных менеджерах?
Менеджеры выходят в обратном порядке входа. Если внутренний __exit__ возвращает истинное значение, исключение считается подавленным, и внешний менеджер получает exc_type=None, будто ошибка не возникала. Если внутренний менеджер не подавил исключение, внешний получает информацию о нём и может либо подавить его, либо передать дальше.