Разберите ситуацию: какие методы контекстных менеджеров будут вызваны, если второй менеджер не смог войти в...

Разберите ситуацию: какие методы контекстных менеджеров будут вызваны, если второй менеджер не смог войти в составной блок 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")
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Сначала вызывается A.__enter__(), затем B.__enter__(). Если B.__enter__() выбрасывает исключение, B.__exit__() не вызывается, потому что второй менеджер не завершил вход в контекст; зато вызывается A.__exit__() для уже успешно открытого менеджера.

Вывод будет таким:

enter A enter B exit A Traceback ... RuntimeError: B

Исторический контекст

Конструкция with появилась для безопасного управления ресурсами, чтобы заменить повторяющийся шаблон с try и finally. Её назначение — гарантировать выполнение завершающей логики даже при исключении в теле блока или при последующих шагах входа в составной контекст.

Составной with концептуально обрабатывается как вложенные контекстные менеджеры. Поэтому порядок входа является прямым, а порядок выхода — обратным.

Постановка проблемы

Программа может открывать несколько ресурсов последовательно: например, соединение и транзакцию, файл и временный каталог или блокировку и связанный объект. Ошибка при открытии второго ресурса не должна оставлять первый ресурс захваченным.

Неверное предположение состоит в том, что __exit__() вызывается у каждого перечисленного менеджера. На самом деле он вызывается только у менеджера, чей __enter__() завершился успешно.

Подробное решение

Составной блок:

with A() as a, B() as b: body()

работает по смыслу как вложенная конструкция:

with A() as a: with B() as b: body()

Сначала вызывается A.__enter__(). После успешного возврата Python вызывает B.__enter__(). Если этот вызов завершается исключением, тело блока не выполняется, B.__exit__() не вызывается, а исключение распространяется через внешний контекст, который вызывает A.__exit__().

У A.__exit__() будут переданы тип исключения, его объект и traceback. В примере метод возвращает False, поэтому исключение не подавляется и после очистки доходит до вызывающего кода.

Если бы A.__enter__() сам выбросил исключение, A.__exit__() также не был бы вызван: вход в этот контекст не завершился успешно. Это важное правило позволяет не выполнять освобождение ресурса, который фактически не был захвачен.

Ситуация из практики

Сервис открывает временный файл, а затем пытается получить блокировку для записи. Возможны два подхода:

  • вручную использовать вложенные try/finally: это гибко, но легко ошибиться в порядке очистки;
  • применять составной with: порядок входа и обратный порядок выхода выражены непосредственно в структуре кода.

Подход с with предпочтительнее, если каждый ресурс предоставляет корректный контекстный менеджер. Если второй ресурс не создаётся, первый всё равно освобождается через его __exit__(), что предотвращает утечки дескрипторов или блокировок.

При этом __enter__() должен освобождать частично захваченные внутренние ресурсы, если ошибка произошла уже внутри самого его тела. Внешний __exit__() для такого менеджера вызван не будет.

Что кандидаты часто упускают

  1. Вызывается ли __exit__() у менеджера, если его __enter__() выбросил исключение?

    Нет. Контракт with вызывает __exit__() только после успешного завершения __enter__(). Если менеджер успел захватить часть ресурсов до ошибки, он должен очистить их сам внутри __enter__() либо делегировать создание ресурса отдельной безопасной операции.

  2. В каком порядке вызываются методы при успешном входе в два контекста?

    Сначала выполняется A.__enter__(), затем B.__enter__(). При нормальном выходе сначала вызывается B.__exit__(), затем A.__exit__(). Такой порядок соответствует стековой модели: последний успешно открытый ресурс закрывается первым.

  3. Что произойдёт, если A.__exit__() подавит исключение, возникшее в B.__enter__()?

    Если A.__exit__() вернёт истинное значение, например True, исключение будет подавлено. Тело блока всё равно не выполнится, а переменная b не получит значение; подавление относится только к распространению исключения наружу и не превращает неудачный вход B в успешный.