После успешного __enter__ присваивание результата в as завершается ошибкой: будет ли вызван __exit__?
Да. Если __enter__ успешно завершился, но присваивание его результата в цель as вызвало исключение, Python всё равно вызовет __exit__. В него будет передана информация именно об ошибке присваивания; тело блока with при этом не начнёт выполняться.
Если __exit__ вернёт True, исключение будет подавлено. При False или любом ложном результате исключение продолжит распространяться после завершения with.
Конструкция with появилась как стандартный механизм безопасного управления ресурсами: файл, соединение или блокировка должны освобождаться не только после нормального завершения блока, но и при любой ошибке внутри его подготовки или выполнения.
Поэтому жизненный цикл контекстного менеджера включает не только тело блока. После успешного входа Python организует защищённый участок, в который входит и присваивание результата __enter__ цели as.
Цель после as может быть не простой переменной, а шаблоном распаковки, атрибутом или элементом контейнера. Такая операция способна завершиться ошибкой: например, при несовпадении числа элементов или при исключении в пользовательском установщике атрибута.
Если ошибочно считать, что __exit__ отвечает только за ошибки тела with, можно пропустить очистку ресурса. Обратная сторона — __exit__ должен быть готов обработать исключение, хотя тело блока фактически ещё не выполнялось.
Упрощённая последовательность такова:
__enter__.__enter__ присваивается цели после as.__exit__ с информацией об исключении либо с пустыми значениями при успехе.После успешного __enter__ шаги 3 и 4 находятся под контролем механизма выхода. Поэтому ошибка на шаге присваивания вызывает __exit__ так же, как ошибка в теле блока. При ошибке в самом __enter__ этот конкретный контекст не считается успешно открытым, поэтому его __exit__ не вызывается.
Распаковка вызывает ValueError, поэтому вывод содержит enter, затем exit: ValueError; строка body не выполняется. Так как __exit__ возвращает False, ошибка достигает внешнего except.
Возвращаемое значение __exit__ имеет тот же смысл, что и при исключении в теле: истинное значение подавляет ошибку. Если сам __exit__ выбросит новое исключение, оно заменит исключение, возникшее при присваивании, поэтому очистку обычно не следует смешивать с операциями, способными неконтролируемо падать.
Сервисный контекстный менеджер возвращает объект соединения, а разработчик использует распаковку результата as в несколько переменных. После изменения API соединение стало возвращать объект с другим количеством полей. В результате вход в ресурс проходит, распаковка падает, а корректно реализованный __exit__ закрывает соединение.
Вариант с распаковкой удобен краткостью, но хрупок при изменении контракта результата. Можно привязать результат к одной переменной и получать поля явно: это увеличивает объём кода, зато делает ошибку понятнее и упрощает совместимость API.
Выбранный подход зависит от контракта. Для публичного или изменяющегося API предпочтительнее возвращать один объект ресурса и использовать явные атрибуты; для стабильного внутреннего протокола распаковка допустима, если __exit__ гарантирует освобождение ресурса.
__exit__, если исключение возникло внутри __enter__?Нет, если сам __enter__ завершился исключением. Успешный вход не был подтверждён, поэтому Python не вызывает __exit__ для этого менеджера. Если внутри __enter__ уже были частично выделены ресурсы, метод должен самостоятельно откатить эти действия перед выбросом исключения.
__exit__ подавит ошибку распаковки?При истинном результате __exit__ исключение считается обработанным, и выполнение продолжится после всего оператора with. Тело блока не будет выполнено задним числом, а переменная или часть цели могут остаться неинициализированными; поэтому подавлять такие ошибки безопасно только при явно продуманном контракте.
with, если во время очистки __exit__ выбросит новое исключение?Наружу обычно выйдет исключение из __exit__, поскольку оно возникает позже и заменяет исходную ошибку присваивания. Исходная ошибка может остаться в цепочке исключений через механизм __context__, но обработчик внешнего уровня получит новым активным исключением именно сбой очистки.