Справочник по Python · модуль 8 · исключения и файлы
Глава 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:
└────────┬────────┘ └─┬┘
контекстный менеджер имя, под которым
он доступен в блоке
тело блока ← здесь ресурс открыт
# здесь уже закрыт
with open("demo.txt", "w", encoding="utf-8") as file: file.write("данные") print(file.closed)
True
Переменная file после блока остаётся доступной — исчезает не она, а открытый ресурс. Обращаться к закрытому файлу нельзя, но проверить closed можно.
§3Закрытие при ошибке
Главное свойство: файл закроется, даже если блок прервался исключением.
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 стоит простой протокол из двух методов.
- Вычисляется выражение после
with— получается объект-менеджер. - У него вызывается
__enter__(). Возвращённое значение связывается с именем послеas. - Выполняется тело блока.
- При выходе — любом: обычном, через
return,breakили из-за исключения — вызывается__exit__().
Никакой магии здесь нет: любой объект с этими двумя методами можно использовать в with. У файлового объекта __enter__() возвращает сам файл, а __exit__() вызывает close().
§6Метод __enter__
Вызывается при входе в блок. Он готовит ресурс и возвращает то, что попадёт в переменную после as.
with open("f.txt") as file:
open(...) создаёт файловый объект
.__enter__() возвращает его же
file = результат ← вот что попадает в переменную
Возвращать можно что угодно — не обязательно сам менеджер. Это позволяет, например, отдать в блок уже подготовленное соединение или курсор базы данных.
§7Метод __exit__
Вызывается при выходе и получает три аргумента — сведения об исключении, если оно было.
| Аргумент | Если ошибки не было | Если была |
|---|---|---|
exc_type | None | класс исключения |
exc_value | None | сам объект ошибки |
traceback | None | объект трассировки |
Это те же данные, что в главе 8.2 лежали в атрибутах исключения. Проверка if exc_type is not None — стандартный способ узнать, штатно ли завершился блок.
§8Свой контекстный менеджер
Чтобы протокол стал наглядным, его удобно увидеть на собственном классе. Классы разбираются в модуле 9, здесь достаточно понимать, что def внутри class — это метод объекта.
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Исключение внутри блока
Тот же менеджер, но теперь блок падает.
try: with MyContextManager() as manager: raise ValueError("Это ошибка для теста") except ValueError as e: print("наружу:", e)
Вход в контекст Выход из контекста Было исключение: Это ошибка для теста наружу: Это ошибка для теста
Видно ровно то, что описано в §4: __exit__() получил сведения об ошибке, выполнил уборку, но исключение продолжило путь наверх.
§10Подавление исключения
Продолжить путь наверх ошибка может не всегда. Возвращаемое значение __exit__() решает её судьбу.
__exit__ вернул | Что происходит с исключением |
|---|---|
False или None | уходит дальше, как обычно |
True | подавляется, программа продолжает работу |
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 можно открыть несколько ресурсов — они перечисляются через запятую и закрываются в обратном порядке.
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Частые ошибки
| Что написано | Что произойдёт | Как правильно |
|---|---|---|
обращение к файлу после блока with | ValueError: 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. Дальше в модуле все примеры работы с файлами используют только эту форму.