Оператор with и менеджеры контекста в Python
Узнайте, как работает оператор with в Python, как создавать менеджеры контекста с __enter__/__exit__ и как использовать contextlib.
Оператор with гарантирует, что ресурсы — такие как файлы, сетевые соединения и блокировки — правильно инициализируются и освобождаются, даже если исключение прерывает выполнение блока. Объект, управляющий этой инициализацией и завершением, называется менеджером контекста.
В этой главе объясняется, как работает оператор with, когда его использовать, как создавать собственные менеджеры контекста с помощью __enter__ и __exit__, а также как создавать лёгкие менеджеры с помощью contextlib.contextmanager.
Зачем нужен with
До появления оператора with управление ресурсами требовало ручного написания блоков try/finally:
f = open("data.txt", "r", encoding="utf-8")
try:
content = f.read()
finally:
f.close() # must always close, even if read() raisesЭто работает, но громоздко, легко забыть и добавляет шаблонный код вокруг каждого ресурса. Оператор with сжимает всё это в один читаемый блок и автоматически выполняет очистку:
with open("data.txt", "r", encoding="utf-8") as f:
content = f.read()
# f is closed here, no matter what happened inside the blockКонструкция as f привязывает значение менеджера контекста к имени f. Некоторые менеджеры контекста не возвращают полезного значения — в этом случае as можно опустить:
with some_lock:
shared_data.append(item)Как работает оператор with
Когда Python выполняет оператор with, он следует такой последовательности:
- Вычисляет выражение после
with— это возвращает объект менеджера контекста. - Вызывает метод
__enter__()менеджера контекста. Возвращаемое значение__enter__()привязывается к переменнойas(если она указана). - Выполняет тело блока
with. - Вызывает метод
__exit__(exc_type, exc_val, exc_tb)менеджера контекста.- Если блок завершился нормально, все три аргумента равны
None. - Если было выброшено исключение, три аргумента описывают его.
- Если
__exit__возвращает истинное значение, исключение подавляется и выполнение продолжается после блокаwith. Если возвращается ложное значение (илиNone), исключение распространяется дальше.
- Если блок завершился нормально, все три аргумента равны
Этот протокол называется протоколом менеджера контекста.
Открытие файлов с помощью with
Наиболее распространённое применение with — работа с файлами. Встроенные файловые объекты Python реализуют протокол менеджера контекста, поэтому они автоматически закрываются по завершении блока:
with open("report.txt", "w", encoding="utf-8") as f:
f.write("Sales: 1 000\n")
f.write("Returns: 23\n")
print(f.closed) # True — file was closed on exitЕсли внутри блока возникает исключение, файл всё равно закрывается:
try:
with open("data.txt", "r", encoding="utf-8") as f:
raise RuntimeError("something went wrong")
except RuntimeError:
pass
print(f.closed) # True — closed despite the exceptionБез with забытый вызов f.close() после ошибки оставляет файловый дескриптор открытым до тех пор, пока не сработает сборщик мусора — или до завершения процесса — что может привести к потере данных или ошибкам «слишком много открытых файлов» в долго работающих программах.
Открытие нескольких ресурсов одновременно
Несколько ресурсов можно открыть в одном операторе with, разделив их запятыми (Python 3.1+):
with open("input.txt", "r", encoding="utf-8") as src, \
open("output.txt", "w", encoding="utf-8") as dst:
for line in src:
dst.write(line.upper())Это полностью эквивалентно вложению двух операторов with, но сохраняет плоский уровень отступа.
Создание менеджера контекста с __enter__ и __exit__
Любой класс, определяющий __enter__ и __exit__, можно использовать с оператором with. Вот минимальный пример — таймер, измеряющий, сколько времени выполняется блок with:
import time
class Timer:
def __enter__(self):
self._start = time.perf_counter()
return self # bound to the 'as' variable
def __exit__(self, exc_type, exc_val, exc_tb):
elapsed = time.perf_counter() - self._start
print(f"Elapsed: {elapsed:.4f}s")
return False # do not suppress exceptions
with Timer() as t:
total = sum(range(1_000_000))
# Elapsed: 0.0xxx s
print(total) # 499999500000Ключевые моменты:
__enter__выполняется до блока. Возвращаемое значение привязывается кas t. Возвратselfпозволяет вызывающему коду обращаться кt.elapsedи другим атрибутам при необходимости.__exit__выполняется после блока, даже при исключении. ВозвратFalse(илиNone) позволяет любому исключению распространяться нормально.
Подавление исключений в __exit__
Если __exit__ возвращает True, исключение поглощается и выполнение продолжается после блока with. Это намеренное поведение в определённых контекстах — например, менеджер контекста, который перехватывает и записывает ошибки, не прерывая работу программы:
class Ignore:
"""Silently ignore any exception raised inside the with block."""
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
print(f"Suppressed: {exc_type.__name__}: {exc_val}")
return True # suppress the exception
with Ignore():
x = 1 / 0 # ZeroDivisionError is caught and ignored
print("execution continues here")
# Suppressed: ZeroDivisionError: division by zero
# execution continues hereИспользуйте подавление исключений осторожно — скрытое поглощение ошибок может маскировать баги. Идиоматический способ сделать это в стандартной библиотеке — contextlib.suppress (см. ниже).
Менеджер контекста для подключения к базе данных
Более реалистичный пример — управление подключением к базе данных с фиксацией транзакции при успехе и откатом при ошибке:
class ManagedTransaction:
def __init__(self, connection):
self.conn = connection
def __enter__(self):
self.conn.begin()
return self.conn
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
self.conn.commit()
else:
self.conn.rollback()
return False # always let exceptions propagateЭтот паттерн — фиксация при успехе, откат при ошибке — встречается во всех реальных библиотеках для работы с базами данных (SQLite, SQLAlchemy, psycopg2 реализуют его).
contextlib.contextmanager: менеджеры контекста на основе генераторов
Написание полноценного класса с __enter__ и __exit__ — правильный подход для сложных или состоятельных менеджеров контекста. Для более простых случаев декоратор contextlib.contextmanager позволяет выразить ту же логику в виде функции-генератора:
from contextlib import contextmanager
@contextmanager
def managed_open(path, mode="r", encoding="utf-8"):
print(f"Opening {path}")
f = open(path, mode, encoding=encoding)
try:
yield f # everything up to yield is __enter__
finally:
f.close() # everything after yield is __exit__
print(f"Closed {path}")
with managed_open("notes.txt", "w") as f:
f.write("hello\n")
# Opening notes.txt
# Closed notes.txtПротокол генератора напрямую соответствует протоколу менеджера контекста:
- Код до
yield→__enter__(инициализация). - Выражение
yield→ значение, привязываемое к переменнойas. - Код после
yield(обычно в блокеfinally) →__exit__(завершение).
Блок try/finally вокруг yield важен: без него исключение внутри блока with приведёт к тому, что код завершения никогда не выполнится.
Пример contextmanager: временная рабочая директория
import os
from contextlib import contextmanager
@contextmanager
def working_directory(path):
original = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(original)
with working_directory("/tmp"):
print(os.getcwd()) # /tmp (or system temp dir)
print(os.getcwd()) # restored to original directoryЭтот паттерн также доступен в стандартной библиотеке в виде tempfile.TemporaryDirectory.
Утилиты contextlib
Модуль contextlib поставляется с несколькими готовыми менеджерами контекста, которые стоит знать:
contextlib.suppress
Подавление конкретных исключений без лишнего шаблонного кода:
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove("temp.txt") # no error even if file does not existЭквивалентно блоку try/except, который ничего не делает при перехваченном исключении.
contextlib.nullcontext
Менеджер контекста, не выполняющий никаких действий, — полезен, когда менеджер контекста нужен условно:
from contextlib import nullcontext
def process(data, lock=None):
ctx = lock if lock is not None else nullcontext()
with ctx:
return sorted(data)Без nullcontext потребовалась бы ветка if lock: каждый раз.
contextlib.ExitStack
ExitStack позволяет управлять динамическим числом менеджеров контекста — полезно, когда количество ресурсов неизвестно до момента выполнения:
from contextlib import ExitStack
files = ["a.txt", "b.txt", "c.txt"]
with ExitStack() as stack:
handles = [
stack.enter_context(open(f, "w", encoding="utf-8"))
for f in files
]
for i, fh in enumerate(handles):
fh.write(f"file {i}\n")
# All three files are closed hereExitStack также является правильным инструментом, когда нужно условно добавить менеджер контекста или отложить очистку на более поздний момент.
Когда использовать with, а когда try/finally
Используйте with всегда, когда:
- Ресурс должен быть освобождён после использования (файлы, сокеты, блокировки, курсоры баз данных).
- Нужно гарантировать очистку даже при исключениях.
- Логика очистки одинакова независимо от успеха или неудачи.
Используйте обычный try/finally только когда:
- Нужны разные действия при очистке в зависимости от типа исключения — хотя
__exit__тоже может это делать. - Пишете код, совместимый с Python 2 (сегодня редкость).
На практике, если объект поддерживает протокол менеджера контекста, всегда предпочитайте with.
Краткий справочник
| Возможность | Что делает |
|---|---|
with expr as v: | Вызывает expr.__enter__(), привязывает результат к v, вызывает __exit__ при выходе |
| Несколько ресурсов | with A() as a, B() as b: — оба освобождаются, даже если B() вызывает исключение |
__enter__(self) | Инициализация; возвращаемое значение привязывается к переменной as |
__exit__(self, exc_type, exc_val, exc_tb) | Завершение; вернуть True, чтобы подавить исключение |
@contextmanager | Превращает функцию-генератор в менеджер контекста |
contextlib.suppress(E) | Поглощает исключение типа E без try/except |
contextlib.nullcontext() | Заглушка, когда менеджер контекста необязателен |
contextlib.ExitStack | Управление динамическим или условным набором менеджеров контекста |
Связанные главы
- Работа с файлами в Python — использование
with open(...)для каждой файловой операции - Try Except в Python — обработка исключений и try/finally
- Декораторы Python —
@contextmanagerиспользует тот же паттерн декораторов - Генераторы Python — как протокол генераторов работает в
@contextmanager - Классы и объекты Python — написание методов
__enter__и__exit__