Как объект, переданный в конструкцию with, участвует в освобождении ресурса при выходе из блока?
Объект должен поддерживать протокол контекстного менеджера: реализовать методы __enter__ и __exit__. Python вызывает __enter__ при входе в блок, а __exit__ — при выходе, в том числе если внутри блока возникло исключение.
Метод __exit__ получает сведения об исключении и своим возвращаемым значением определяет, подавлять его или передавать дальше. Возврат False или None сохраняет обычное распространение исключения.
Конструкция with была введена для стандартизации шаблона безопасного управления ресурсами, который раньше обычно записывали через try и finally. Такой подход уменьшает риск забыть освобождение файла, блокировки, соединения или другого ресурса на одном из путей выполнения.
В Python этот механизм основан на протоколе, а не на наследовании от обязательного базового класса. Поэтому контекстным менеджером может быть любой объект с подходящими специальными методами; для генераторных сценариев также применяется декоратор contextlib.contextmanager.
Ресурс нужно освобождать не только после успешного выполнения операции, но и при исключении. Если очистка размещена только после основного действия, ошибка прервет выполнение и оставит ресурс занятым.
Дополнительная опасность связана с подавлением исключений. Если контекстный менеджер без необходимости возвращает истинное значение из __exit__, исходная ошибка исчезает для вызывающего кода, что затрудняет диагностику и может скрыть повреждение состояния.
При входе в with Python вызывает __enter__. Его результат можно связать с переменной через as; это не обязательно сам контекстный менеджер, а именно значение, возвращенное __enter__.
После завершения тела блока Python вызывает __exit__(тип_исключения, значение_исключения, трассировка). При нормальном завершении все три аргумента равны None. При исключении контекстный менеджер может выполнить откат, закрыть ресурс или освободить блокировку.
В этом примере close будет напечатано даже при исключении, после чего ValueError продолжит распространяться, поскольку __exit__ возвращает False. Если вернуть True, исключение будет подавлено; это допустимо только при осознанной политике обработки ошибок.
Важная граница: если __enter__ завершился исключением, __exit__ для этого объекта не вызывается. Поэтому частичное выделение ресурса внутри __enter__ нужно либо откатывать там же, либо организовывать так, чтобы ресурс создавался до входа в контекст и имел отдельную защиту.
В сервисе обработка сообщения выполняет несколько операций внутри транзакции. При успехе нужен commit, при любой ошибке — rollback, причём ошибка должна попасть в систему повторной обработки.
Рассматривались три варианта. Ручной try и finally даёт полный контроль, но легко приводит к дублированию логики и ошибкам в новых ветках. Декоратор contextmanager делает простой сценарий компактным, однако при сложной политике транзакции состояние и несколько этапов завершения могут быть менее очевидны. Классический контекстный менеджер явно разделяет вход, выход и решение о подавлении исключения.
Выбран был класс с вызовом commit только при отсутствии исключения, rollback при наличии исключения и возвратом False из __exit__. Это сохранило единое правило завершения транзакции, гарантировало освобождение соединения и не скрыло исходную ошибку от обработчика сообщений.
Что произойдёт, если __enter__ вернёт объект, отличный от самого контекстного менеджера?
Именно возвращённое значение будет присвоено переменной после as. При этом __exit__ всё равно вызывается у исходного объекта, участвующего в конструкции with, а не у значения, возвращённого __enter__. Это позволяет, например, скрыть внутреннюю обёртку и предоставить пользователю интерфейс ресурса.
Как контекстный менеджер должен поступить с исключением, возникшим внутри блока?
Он получает тип, экземпляр и трассировку исключения в __exit__. Менеджер может преобразовать ошибку, выбросив другую, или подавить её возвратом истинного значения, но подавление должно быть частью явно заданной семантики. Для обычного менеджера ресурсов безопасное значение по умолчанию — вернуть False и не скрывать исключение.
Почему освобождение ресурса в __exit__ не гарантирует корректную очистку при ошибке в __enter__?
Метод __exit__ вызывается только после успешного завершения __enter__. Если __enter__ успел частично открыть ресурс, но затем завершился ошибкой, очистку нужно выполнить внутри __enter__ при обработке этой ошибки или передать ответственность другому уже активному менеджеру. Иначе частично созданный ресурс может остаться без владельца и не быть освобождённым.