2.24. Менеджеры контекста

Оператор with выполняет код настройки, затем тело, затем код завершения – с гарантией, что код завершения выполнится, даже если тело завершится с ошибкой на полпути. Пара методов, обеспечивающая настройку и завершение, называется менеджером контекста.

Форма такова:

with <expression> as <name>:
    <body>

Выражение возвращает менеджер контекста. Python вызывает его метод __enter__, при необходимости связывает результат с <name>, выполняет тело, а затем вызывает __exit__ – независимо от того, завершилось ли тело нормально или возбудило исключение.

Диаграмма потока: сначала выполняется __enter__, затем тело; тело либо завершается нормально, либо возбуждает исключение; в любом случае в конце выполняется __exit__.

__exit__ выполняется в конце блока, что бы внутри него ни происходило.

2.24.1. Использование менеджера контекста

Канонический пример – открытие файла:

with open("data.txt") as f:
    text = f.read()

# f is now closed, even if read() failed

open() возвращает файловый объект, который сам является менеджером контекста; __enter__ возвращает файл, а __exit__ закрывает его. Блок with делает правило «всегда закрывать файл по завершении» поведением по умолчанию, а не тем, что вызывающий должен помнить.

2.24.1.1. Несколько менеджеров контекста

Один оператор with может войти сразу в несколько менеджеров контекста, разделённых запятыми:

with open("input.txt") as src, open("output.txt", "w") as dst:
    dst.write(src.read())

Эквивалентно двум вложенным блокам with, но более плоско. Менеджеры входятся слева направо и выходятся в обратном порядке; если __enter__ у правого менеджера возбуждает исключение, __exit__ левого менеджера всё равно выполнится.

2.24.2. Написание менеджера контекста

Любой класс с методами __enter__ и __exit__ работает как менеджер контекста:

class Section:
    def __init__(self, label):
        self.label = label

    def __enter__(self):
        print("---", self.label, "---")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("--- end", self.label, "---")
        return False

with Section("setup"):
    print("doing the work")

Вывод:

--- setup ---
doing the work
--- end setup ---

__exit__ получает три аргумента, описывающих исключение, завершившее блок, или три значения None, если блок завершился нормально. Возврат False (или None) позволяет любому исключению распространиться после завершения; возврат True подавил бы его.

Используйте менеджеры контекста для любого ресурса, имеющего жизненный цикл «открыть / закрыть» или «захватить / освободить» – не только для файлов. Этот шаблон держит очистку в паре с настройкой в той точке, где обе вводятся, поэтому забытый вызов закрытия в середине длинной функции не может привести к утечке ресурса.