Контекстные менеджеры Python: with и гарантированная очистка

Контекстный менеджер связывает получение ресурса с обязательным завершением работы. Инструкция with вызывает вход в контекст, выполняет блок и затем запускает выход даже при…

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

Протокол входа и выхода

Объект контекстного менеджера реализует __enter__ и __exit__. Значение, возвращённое __enter__, попадает после as. В __exit__ приходят сведения об исключении либо три значения None, если блок завершён успешно. Ложный результат __exit__ не подавляет исключение, истинный — сообщает, что оно обработано. Возвращать True по умолчанию опасно: так можно скрыть программную ошибку.

Менеджер на основе contextmanager

Декоратор contextlib.contextmanager позволяет выразить тот же жизненный цикл генераторной функцией. Код до yield выполняет подготовку, значение yield передаётся в блок, а секция finally гарантирует очистку. Такая форма удобна для короткого менеджера без отдельного состояния и множества методов.

from contextlib import contextmanager
from pathlib import Path
from typing import Iterator, TextIO


@contextmanager
def opened_report(path: Path) -> Iterator[TextIO]:
    handle = path.open('w', encoding='utf-8')
    try:
        print('открыт')
        yield handle
    finally:
        handle.close()
        print('закрыт')


report_path = Path('report.txt')
with opened_report(report_path) as report:
    report.write('готово\n')
    print('записано')

print(report_path.read_text(encoding='utf-8').strip())

Ожидаемый результат

открыт
записано
закрыт
готово

Файл закрывается до следующего чтения. В реальном коде Path.open уже возвращает контекстный менеджер, поэтому достаточно with path.open(...) as report. Обёртка здесь нужна, чтобы явно увидеть стадии и научиться создавать менеджер для более сложного ресурса.

Поведение при исключении

Если внутри блока после записи выполнить raise RuntimeError('сбой'), секция finally всё равно напечатает закрыт, после чего RuntimeError выйдет наружу. В менеджере на основе @contextmanager исключение возникает в точке yield. Его можно поймать вокруг yield, но подавлять следует только тот конкретный случай, который менеджер способен полностью обработать.

Несколько менеджеров можно перечислить в одном with. Они входят слева направо, а выходят в обратном порядке. Это похоже на аккуратно вложенные блоки и важно, когда второй ресурс зависит от первого.

Как диагностировать ошибки жизненного цикла

Характерная ошибка при создании генераторного менеджера — выполнить yield дважды или не выполнить его ни разу. contextlib сообщит RuntimeError о том, что генератор не выдал значение или не остановился. Проверьте, что нормальный путь содержит ровно один yield, а очистка находится в finally.

Если файл остаётся открытым, найдите ранний return или исключение между получением ресурса и входом в try. Ресурс нужно получить непосредственно перед try либо использовать уже готовый менеджер. Для проверки состояния файлового объекта после блока доступен атрибут closed.

Практикум: временное состояние словаря

Создайте менеджер temporary_flag(mapping, key): при входе он сохраняет прежнее наличие и значение ключа, устанавливает True, а при выходе восстанавливает исходное состояние даже после исключения. Самостоятельная проверка: протестируйте существующий и отсутствующий ключ, затем намеренно поднимите ValueError внутри with; после перехвата словарь должен точно совпасть с исходным, а ошибка не должна быть молча подавлена.

Гарантии with и правила очистки

with задаёт границу владения ресурсом и гарантирует вызов выхода. Для класса существуют __enter__ и __exit__, для компактной функции — @contextmanager с одним yield. Очистку помещают в finally, а исключения подавляют только при явном и полном восстановлении.

Вопросы о повторном использовании контекстов

Закрывает ли with ресурс при return внутри блока?

Да. Выход из блока через return, break или исключение всё равно запускает завершение контекста.

Можно ли повторно использовать один генераторный менеджер?

Один созданный экземпляр генераторного менеджера рассчитан на один вход. Для нового блока снова вызовите декорированную функцию.

Всегда ли нужен собственный контекстный менеджер?

Нет. Сначала проверьте API ресурса: файлы, блокировки и многие соединения уже поддерживают with. Собственная обёртка нужна для нового жизненного цикла или композиции действий.

Источники