Menu
Русский
Попробовать в Playground

Контекстные менеджеры в Python: оператор with как он есть

Что на самом деле делает оператор with — автоматическая уборка для файлов, блокировок, коннектов к БД и всего, что требует надёжного закрытия.

Оператор, прибирающийся за собой

Любой открытый в программе ресурс — файл, сетевое соединение, дескриптор БД, блокировка — нужно закрывать, когда закончил. Забудешь — утечка памяти, зависшие блокировки, битые файлы при падении. 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) запускается в конце блока независимо от того, как он закончился. Если причина выхода — исключение, его данные передаются сюда, чтобы менеджер мог их осмотреть или подавить.

Минимальный пример — таймер блока, который он оборачивает:

main.py
Output
Click Run to see the output here.

with Timer(): создаёт объект, вызывает его __enter__, выполняет тело, вызывает __exit__. Ни файлов, ни блокировок — просто маленькая обёртка для «сделай что-то, измерь, сколько заняло».

Сокращение через contextlib.contextmanager

Определять класс ради каждого контекстного менеджера — тяжелее, чем надо. contextlib.contextmanager превращает функцию-генератор в контекстный менеджер: один yield разделяет «до» и «после»:

main.py
Output
Click Run to see the output here.

Всё до yield — поведение __enter__. Всё после — __exit__. try/finally заставляет уборку срабатывать, даже если тело упадёт.

Большинство собственных контекстных менеджеров, которые ты напишешь, укладываются в эту форму. Тянись к декораторной версии в первую очередь; к классу опускайся, только когда нужно что-то, чего генераторная форма не выражает.

Временная смена чего-то

Частый паттерн: установить, использовать, восстановить. Контекстные менеджеры выражают это чисто:

main.py
Output
Click Run to see the output here.

Любой паттерн «установил, потом восстановил» — переменные окружения, уровень логов, feature-флаги, фикстуры тестов — хорошо ложится на контекстный менеджер. Вызывающим не нужно помнить об откате.

Подавление исключений

Метод __exit__ может вернуть True, чтобы сказать Python «я обработал исключение, проглоти его». Это редкость и обычно плохой запах, но именно так работает contextlib.suppress:

main.py
Output
Click Run to see the output here.

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(path) as f: открывает файл и связывает его с f на время блока. Когда блок заканчивается — нормально или из-за исключения — Python автоматически закрывает файл. f.close() вызывать не нужно; with это гарантирует.

Почему лучше with, а не голый open()?

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

Как открыть несколько файлов в одном with?

Через запятую: with open('a.txt') as a, open('b.txt') as b:. Оба файла открываются на входе и закрываются на выходе в обратном порядке. Это заменяет вложенные with, когда ресурсов нужно несколько разом.

Учитесь программировать с Coddy

НАЧАТЬ