Оператор, прибирающийся за собой
Любой открытый в программе ресурс — файл, сетевое соединение, дескриптор БД, блокировка — нужно закрывать, когда закончил. Забудешь — утечка памяти, зависшие блокировки, битые файлы при падении. with в Python решает это за тебя.
Паттерн, с которого все начинают, — чтение файла:
with open("notes.txt") as f:
contents = f.read()
print(contents)
Две вещи происходят автоматически. На входе open() даёт тебе объект файла, связанный с f. На выходе — неважно, нормально закончился блок, был return или исключение — Python вызовет f.close() за тебя.
Вот и всё. Вся суть with.
Что заменяет with
До контекстных менеджеров эквивалентный безопасный код был try/finally:
f = open("notes.txt")
try:
contents = f.read()
print(contents)
finally:
f.close()
Пять строк церемонии ради «прочитай файл и закрой». Умножь это на каждый open в большой программе — и станет понятно, в чём соль. with короче, труднее сломать и невозможно забыть уборку.
Несколько ресурсов
В одном with можно привязать несколько контекстных менеджеров:
with open("input.txt") as src, open("output.txt", "w") as dst:
dst.write(src.read().upper())
Оба файла открываются на входе и закрываются на выходе. Если первый open успешен, а второй падает — Python всё равно закроет первый; механика правильно обрабатывает частичную установку.
Для длинных списков ресурсов форма со скобками (Python 3.10+) понятнее:
with (
open("a.txt") as a,
open("b.txt") as b,
open("c.txt") as c,
):
...
Что такое контекстный менеджер на самом деле
Любой объект, определяющий __enter__ и __exit__, — контекстный менеджер. Протокол до безобразия прост:
__enter__(self)запускается в начале блокаwith. То, что он возвращает, присваивается переменной послеas.__exit__(self, exc_type, exc_value, traceback)запускается в конце блока независимо от того, как он закончился. Если причина выхода — исключение, его данные передаются сюда, чтобы менеджер мог их осмотреть или подавить.
Минимальный пример — таймер блока, который он оборачивает:
with Timer(): создаёт объект, вызывает его __enter__, выполняет тело, вызывает __exit__. Ни файлов, ни блокировок — просто маленькая обёртка для «сделай что-то, измерь, сколько заняло».
Сокращение через contextlib.contextmanager
Определять класс ради каждого контекстного менеджера — тяжелее, чем надо. contextlib.contextmanager превращает функцию-генератор в контекстный менеджер: один yield разделяет «до» и «после»:
Всё до yield — поведение __enter__. Всё после — __exit__. try/finally заставляет уборку срабатывать, даже если тело упадёт.
Большинство собственных контекстных менеджеров, которые ты напишешь, укладываются в эту форму. Тянись к декораторной версии в первую очередь; к классу опускайся, только когда нужно что-то, чего генераторная форма не выражает.
Временная смена чего-то
Частый паттерн: установить, использовать, восстановить. Контекстные менеджеры выражают это чисто:
Любой паттерн «установил, потом восстановил» — переменные окружения, уровень логов, feature-флаги, фикстуры тестов — хорошо ложится на контекстный менеджер. Вызывающим не нужно помнить об откате.
Подавление исключений
Метод __exit__ может вернуть True, чтобы сказать Python «я обработал исключение, проглоти его». Это редкость и обычно плохой запах, но именно так работает contextlib.suppress:
suppress(FileNotFoundError) превращает FileNotFoundError в ничего-не-делание. Используй для по-настоящему необязательных операций — «попробуй, если не вышло — неважно». Не используй, чтобы глушить исключения, о которых не подумал.
Другие контекстные менеджеры, которые встретишь
Контекстные менеджеры раскиданы по стандартной библиотеке, как только начинаешь их замечать:
import threading
from pathlib import Path
# Locks — guarantee release even if the critical section raises.
lock = threading.Lock()
with lock:
...
# tempfile — delete the temp file when you're done.
from tempfile import TemporaryDirectory
with TemporaryDirectory() as tmp:
path = Path(tmp) / "scratch.txt"
path.write_text("hello")
# Database connections — close the connection (or end the transaction).
import sqlite3
with sqlite3.connect(":memory:") as conn:
conn.execute("CREATE TABLE t (x INTEGER)")
Сторонние библиотеки следуют тем же соглашениям. Когда видишь with something as x:, это почти всегда означает «используй x внутри блока, а потом приберись».
Когда with не нужен
- Когда нет настоящей установки и уборки. Оборачивать произвольный код в контекстный менеджер просто так добавляет шума.
- Когда ресурс нужен через много несвязанных блоков. Держать
withоткрытым на всю жизнь длинного скрипта прячет, где на самом деле область уборки. Лучше взять класс, владеющий ресурсом. - Когда декоратор подходит лучше. Некоторые повторяющиеся паттерны (повторы, логи, тайминг) читаются естественнее как
@decoratorнад функцией, чем какwith ...:внутри. Выбирай, что лучше читается на месте вызова.
В большинстве случаев with — правильный выбор. Редкие исключения легко замечать, когда начинаешь их искать.
Дальше: работа с реальными файлами
Теперь ты знаешь, что на самом деле стоит за with open(...) as f: — это и есть тот контекст, в котором ты будешь им пользоваться в девяноста процентах случаев. Следующая глава ставит его на практику — чтение, запись и навигация по файлам на диске.
Часто задаваемые вопросы
Что делает with open в Python?
with open в Python?with open(path) as f: открывает файл и связывает его с f на время блока. Когда блок заканчивается — нормально или из-за исключения — Python автоматически закрывает файл. f.close() вызывать не нужно; with это гарантирует.
Почему лучше with, а не голый open()?
with, а не голый open()?Потому что with закрывает файл, даже если посреди блока всплывёт исключение. С голым open() закрытие остаётся на тебе во всех ветках кода, включая ветки с ошибками. with безопаснее и короче.
Как открыть несколько файлов в одном with?
with?Через запятую: with open('a.txt') as a, open('b.txt') as b:. Оба файла открываются на входе и закрываются на выходе в обратном порядке. Это заменяет вложенные with, когда ресурсов нужно несколько разом.