Разберите ситуацию: какие методы контекстных менеджеров будут вызваны, если второй менеджер не смог войти в составной блок with?
class M:
def __init__(self, name, fail=False):
self.name, self.fail = name, fail
def __enter__(self):
print("enter", self.name)
if self.fail:
raise RuntimeError(self.name)
return self
def __exit__(self, *exc):
print("exit", self.name)
return False
with M("A") as a, M("B", True) as b:
print("body")
Сначала вызывается A.__enter__(), затем B.__enter__(). Если B.__enter__() выбрасывает исключение, B.__exit__() не вызывается, потому что второй менеджер не завершил вход в контекст; зато вызывается A.__exit__() для уже успешно открытого менеджера.
Вывод будет таким:
Конструкция with появилась для безопасного управления ресурсами, чтобы заменить повторяющийся шаблон с try и finally. Её назначение — гарантировать выполнение завершающей логики даже при исключении в теле блока или при последующих шагах входа в составной контекст.
Составной with концептуально обрабатывается как вложенные контекстные менеджеры. Поэтому порядок входа является прямым, а порядок выхода — обратным.
Программа может открывать несколько ресурсов последовательно: например, соединение и транзакцию, файл и временный каталог или блокировку и связанный объект. Ошибка при открытии второго ресурса не должна оставлять первый ресурс захваченным.
Неверное предположение состоит в том, что __exit__() вызывается у каждого перечисленного менеджера. На самом деле он вызывается только у менеджера, чей __enter__() завершился успешно.
Составной блок:
работает по смыслу как вложенная конструкция:
Сначала вызывается A.__enter__(). После успешного возврата Python вызывает B.__enter__(). Если этот вызов завершается исключением, тело блока не выполняется, B.__exit__() не вызывается, а исключение распространяется через внешний контекст, который вызывает A.__exit__().
У A.__exit__() будут переданы тип исключения, его объект и traceback. В примере метод возвращает False, поэтому исключение не подавляется и после очистки доходит до вызывающего кода.
Если бы A.__enter__() сам выбросил исключение, A.__exit__() также не был бы вызван: вход в этот контекст не завершился успешно. Это важное правило позволяет не выполнять освобождение ресурса, который фактически не был захвачен.
Сервис открывает временный файл, а затем пытается получить блокировку для записи. Возможны два подхода:
try/finally: это гибко, но легко ошибиться в порядке очистки;with: порядок входа и обратный порядок выхода выражены непосредственно в структуре кода.Подход с with предпочтительнее, если каждый ресурс предоставляет корректный контекстный менеджер. Если второй ресурс не создаётся, первый всё равно освобождается через его __exit__(), что предотвращает утечки дескрипторов или блокировок.
При этом __enter__() должен освобождать частично захваченные внутренние ресурсы, если ошибка произошла уже внутри самого его тела. Внешний __exit__() для такого менеджера вызван не будет.
Вызывается ли __exit__() у менеджера, если его __enter__() выбросил исключение?
Нет. Контракт with вызывает __exit__() только после успешного завершения __enter__(). Если менеджер успел захватить часть ресурсов до ошибки, он должен очистить их сам внутри __enter__() либо делегировать создание ресурса отдельной безопасной операции.
В каком порядке вызываются методы при успешном входе в два контекста?
Сначала выполняется A.__enter__(), затем B.__enter__(). При нормальном выходе сначала вызывается B.__exit__(), затем A.__exit__(). Такой порядок соответствует стековой модели: последний успешно открытый ресурс закрывается первым.
Что произойдёт, если A.__exit__() подавит исключение, возникшее в B.__enter__()?
Если A.__exit__() вернёт истинное значение, например True, исключение будет подавлено. Тело блока всё равно не выполнится, а переменная b не получит значение; подавление относится только к распространению исключения наружу и не превращает неудачный вход B в успешный.