В следующем контекстном менеджере ошибка возникает и в теле блока, и при выходе. Какое исключение в итоге покинет with?
class Resource:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
if exc_type is not None:
raise ValueError("cleanup failed")
return False
with Resource():
raise KeyError("body failed")
Из блока with выйдет ValueError("cleanup failed"). Исключение KeyError из тела блока станет его контекстом в __context__, но не будет основным исключением, распространяемым наружу.
Протокол контекстного менеджера был введён, чтобы единообразно отделить использование ресурса от его гарантированного освобождения. Конструкция with вызывает __exit__ даже при исключении в теле блока, поэтому код очистки может сам сообщить о собственной ошибке.
Такой протокол решает проблему дублирования try/finally, но требует учитывать приоритет ошибок: сбой очистки может заменить исходную ошибку операции.
После возникновения KeyError Python вызывает __exit__(KeyError, экземпляр_ошибки, traceback). В данном примере __exit__ не возвращает True, потому что до return False управление не доходит: вместо этого он выбрасывает новый ValueError.
Если ошибка очистки скрывает исходную ошибку, диагностика основной проблемы может усложниться. Особенно опасно это для закрытия файлов, соединений, транзакций и других ресурсов, где ошибка освобождения не всегда важнее ошибки основной операции.
Упрощённо поведение with при исключении можно представить так:
Это не буквальная реализация, а модель протокола. Если __exit__ нормально завершился и вернул истинное значение, исходное исключение подавляется. Если он вернул ложное значение, исходное исключение продолжает распространяться.
В рассматриваемом случае __exit__ сам выбрасывает ValueError. Поэтому его возвращаемое значение не анализируется, а ValueError становится исключением, которое покидает with. Python сохраняет исходный KeyError как ValueError.__context__, что позволяет изучить цепочку исключений.
Практический компромисс зависит от политики очистки. Если ошибка освобождения критична, её можно выбросить наружу; если важнее сохранить исходную ошибку, код очистки должен обработать собственный сбой, записать его в журнал или явно объединить ошибки, а не безусловно заменять исключение тела.
Сервис обновляет данные в транзакции, а в __exit__ закрывает соединение. Запрос в теле блока завершился ошибкой валидации, но закрытие соединения также выбросило исключение. Если просто выбросить ошибку закрытия, разработчик сначала увидит проблему инфраструктуры и может пропустить исходную ошибку данных.
Возможны три подхода. Безусловно выбрасывать ошибку очистки просто, но это скрывает исходную причину. Полностью игнорировать её опасно: можно потерять сведения о повреждённом или неосвобождённом ресурсе. Более сбалансированный вариант — сохранить исходное исключение главным, а сбой очистки отправить в журнал или систему мониторинга; для критичных ресурсов политика может быть обратной.
Для обычного соединения с базой данных обычно выбирают сохранение исходной ошибки операции и отдельное логирование сбоя закрытия. Для нарушения целостности транзакции или невозможности гарантированно освободить критичный ресурс выбрасывание ошибки очистки может быть оправдано.
__exit__ выбросит новое исключение после нормального завершения тела with?Ответ: Новое исключение всё равно покинет with, потому что __exit__(None, None, None) вызывается при обычном выходе и может сам завершиться исключением. Подавлять исходное исключение в этом случае нечего; выброшенная из __exit__ ошибка станет основной.
KeyError, если наружу уже вышел ValueError?Ответ: Его можно найти через __context__ у обработанного ValueError, если исключение не было выброшено с подавлением контекста через raise ... from None. Например, обработчик верхнего уровня может проверить error.__context__ и построить полную диагностическую цепочку.
__exit__ не помогает подавить ошибку, если внутри __exit__ выполнен raise?Ответ: Возврат значения происходит только при нормальном завершении метода. Оператор raise немедленно прерывает выполнение __exit__, поэтому Python не получает его результат и не может применить правило подавления исходного исключения. Чтобы подавить исходную ошибку, метод должен завершиться обычным образом и вернуть истинное значение.