Модуль 8 · Исключения и файлы

Справочник по Python · модуль 8 · исключения и файлы

Глава 8.5

8.5

Контекстный
менеджер with

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

Категория
Файлы и ресурсы
Уровень
Базовый → продвинутый
Связанные темы
Исключения, finally, файлы, классы
Зачем это знать
Гарантированно освобождать ресурсы

§1Проблема, которую решает with

Ручное управление файлом требует дисциплины и всё равно ненадёжно.

Сравнениетри подхода к одной задаче
file = open(path)          close() не выполнится при ошибке
...
file.close()

file = open(path)          работает, но многословно
try:
    ...
finally:
    file.close()

with open(path) as file:   то же самое, три слова
    ...

Контекстный менеджер — объект, который умеет сам подготовить ресурс при входе в блок и освободить при выходе. Файл — самый частый его пример, но не единственный.

§2Синтаксис

Схемаустройство конструкции
with open("file.txt", "r") as file:
     └────────┬────────┘    └─┬┘
      контекстный менеджер   имя, под которым
                             он доступен в блоке
    тело блока               ← здесь ресурс открыт

# здесь уже закрыт
main.pyзапись и автоматическое закрытие
with open("demo.txt", "w", encoding="utf-8") as file:
    file.write("данные")

print(file.closed)
Выводфайл закрыт после выхода из блока
True

Переменная file после блока остаётся доступной — исчезает не она, а открытый ресурс. Обращаться к закрытому файлу нельзя, но проверить closed можно.

§3Закрытие при ошибке

Главное свойство: файл закроется, даже если блок прервался исключением.

main.pyошибка посреди работы с файлом
try:
    with open("demo.txt", "w", encoding="utf-8") as file:
        file.write("данные")
        1 / 0
except ZeroDivisionError:
    print("исключение поймано снаружи")

print("файл закрыт:", file.closed)
print("содержимое:", open("demo.txt", encoding="utf-8").read())
Выводданные записаны, ресурс освобождён
исключение поймано снаружи
файл закрыт: True
содержимое: данные

Обратите внимание: записанное попало на диск. Закрытие сбрасывает буфер, о котором пойдёт речь в главе 8.12, — без него данные могли бы потеряться.

§4with не перехватывает

Частое заблуждение: раз with «обрабатывает исключения», он их и глушит. Это не так — в примере выше ошибка вышла наружу и была поймана внешним try.

КонструкцияОсвобождает ресурсПерехватывает ошибку
withданет
try / finallyда, если написатьнет
try / exceptнетда

with — это finally, а не except. Если ошибку нужно ещё и обработать, конструкции сочетают: with внутри try, как в примере §3.

§5Что делает Python

За with стоит простой протокол из двух методов.

  1. Вычисляется выражение после with — получается объект-менеджер.
  2. У него вызывается __enter__(). Возвращённое значение связывается с именем после as.
  3. Выполняется тело блока.
  4. При выходе — любом: обычном, через return, break или из-за исключения — вызывается __exit__().

Никакой магии здесь нет: любой объект с этими двумя методами можно использовать в with. У файлового объекта __enter__() возвращает сам файл, а __exit__() вызывает close().

§6Метод __enter__

Вызывается при входе в блок. Он готовит ресурс и возвращает то, что попадёт в переменную после as.

Схемаоткуда берётся file
with open("f.txt") as file:

     open(...)          создаёт файловый объект
     .__enter__()       возвращает его же
     file = результат   ← вот что попадает в переменную

Возвращать можно что угодно — не обязательно сам менеджер. Это позволяет, например, отдать в блок уже подготовленное соединение или курсор базы данных.

§7Метод __exit__

Вызывается при выходе и получает три аргумента — сведения об исключении, если оно было.

АргументЕсли ошибки не былоЕсли была
exc_typeNoneкласс исключения
exc_valueNoneсам объект ошибки
tracebackNoneобъект трассировки

Это те же данные, что в главе 8.2 лежали в атрибутах исключения. Проверка if exc_type is not None — стандартный способ узнать, штатно ли завершился блок.

§8Свой контекстный менеджер

Чтобы протокол стал наглядным, его удобно увидеть на собственном классе. Классы разбираются в модуле 9, здесь достаточно понимать, что def внутри class — это метод объекта.

main.pyменеджер, который печатает свои шаги
class MyContextManager:
    def __enter__(self):
        print("Вход в контекст")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("Выход из контекста")
        if exc_type is not None:
            print("Было исключение:", exc_value)
        return False

with MyContextManager() as manager:
    print("тело блока")
Выводпорядок вызовов виден целиком
Вход в контекст
тело блока
Выход из контекста

§9Исключение внутри блока

Тот же менеджер, но теперь блок падает.

main.pyошибка в теле with
try:
    with MyContextManager() as manager:
        raise ValueError("Это ошибка для теста")
except ValueError as e:
    print("наружу:", e)
Вывод__exit__ отработал, ошибка ушла дальше
Вход в контекст
Выход из контекста
Было исключение: Это ошибка для теста
наружу: Это ошибка для теста

Видно ровно то, что описано в §4: __exit__() получил сведения об ошибке, выполнил уборку, но исключение продолжило путь наверх.

§10Подавление исключения

Продолжить путь наверх ошибка может не всегда. Возвращаемое значение __exit__() решает её судьбу.

__exit__ вернулЧто происходит с исключением
False или Noneуходит дальше, как обычно
Trueподавляется, программа продолжает работу
main.pyменеджер, который гасит ошибки
class Suppress:
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("подавляю:", exc_value)
        return True

with Suppress():
    raise ValueError("исчезнет")

print("после блока — программа жива")
Выводисключение не вышло наружу
подавляю: исчезнет
после блока — программа жива

Отсюда важное следствие: явный return False в __exit__() — не формальность. Забытый return вернёт None, что тоже означает «не подавлять», но писать намерение явно надёжнее.

§11Несколько ресурсов сразу

В одном with можно открыть несколько ресурсов — они перечисляются через запятую и закрываются в обратном порядке.

main.pyкопирование файла
with open("demo.txt", encoding="utf-8") as src, \
     open("copy.txt", "w", encoding="utf-8") as dst:
    dst.write(src.read())

print(open("copy.txt", encoding="utf-8").read())
Выводсодержимое скопировано
данные

Если второй файл не откроется, первый всё равно будет корректно закрыт. Вложенные with дали бы тот же результат, но запись через запятую короче.

§12Где ещё применяется

Файлы — самый частый случай, но протокол общий. Он применим везде, где ресурс нужно освободить.

РесурсЧто делает выход из блока
файлсбрасывает буфер и закрывает
сетевое соединениеразрывает соединение
соединение с базой данныхфиксирует или откатывает транзакцию
блокировкаснимает блокировку

Общий признак: всё, что нужно «взять и обязательно вернуть». Если в документации библиотеки объект предлагают использовать через with — значит, у него есть что освобождать.

§13with или try/finally

Обе конструкции гарантируют уборку. Разница в том, кто за неё отвечает.

Сравнениеодно и то же двумя способами
file = open(path)          уборку пишет тот, кто использует
try:
    ...
finally:
    file.close()

with open(path) as file:   уборку знает сам объект
    ...

Практическое правило: если у объекта есть контекстный менеджер — использовать его. try/finally остаётся для случаев, когда менеджера нет или уборка нестандартная.

§14Частые ошибки

Что написаноЧто произойдётКак правильно
обращение к файлу после блока withValueError: I/O operation on closed fileработать внутри блока
ждать, что with перехватит ошибкуисключение уйдёт наружуобернуть with в try
file.close() внутри withлишнее: закрытие произойдёт самоубрать
with open(path) без asфайл откроется, но обратиться к нему нельзядобавить as file
два вложенных with ради двух файловлишний уровень отступаперечислить через запятую
return True в __exit__() по невнимательностивсе ошибки в блоке исчезнутreturn False
открывать файл в цикле без withнакапливаются незакрытые файлыwith внутри цикла

§15Проверьте себя

Что гарантирует with?

Что ресурс будет освобождён при любом выходе из блока — обычном, через return или из-за исключения.

Перехватывает ли with исключения?

Нет. Он ведёт себя как finally, а не как except: уборку делает, ошибку пропускает наверх.

Какие два метода образуют протокол?

__enter__() готовит ресурс и возвращает то, что попадёт в переменную после as. __exit__() выполняет уборку и получает сведения об исключении.

Как __exit__() может подавить исключение?

Вернув True. Значение False или None означает «пропустить ошибку дальше».

Доступна ли переменная после блока with?

Да, но ресурс уже освобождён: у файла closed будет True, и работать с ним нельзя.

Когда всё-таки нужен try/finally?

Когда у объекта нет контекстного менеджера или уборка нестандартная. Если менеджер есть — используют его.

§16Шпаргалка

Файлwith open(path) as f:
Два файлаwith open(a) as x, open(b) as y:
С обработкойtry: with ... except ...
Свой менеджер__enter____exit__
Подавить ошибкуreturn True в __exit__
Пропустить дальшеreturn False

Итог главы

with освобождает ресурс при любом выходе из блока и потому заменяет пару open() и close(), которая ломается на первом же исключении. При этом он не перехватывает ошибки — ведёт себя как finally, а не как except. За конструкцией стоит протокол из двух методов: __enter__() готовит ресурс и отдаёт значение для as, __exit__() убирает за собой и может подавить исключение, вернув True. Дальше в модуле все примеры работы с файлами используют только эту форму.

Глава 8.5 · Контекстный менеджер with · Справочник по Python Автор: Макаров Максим Николаевич