W3docs

Оператор 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, он следует такой последовательности:

  1. Вычисляет выражение после with — это возвращает объект менеджера контекста.
  2. Вызывает метод __enter__() менеджера контекста. Возвращаемое значение __enter__() привязывается к переменной as (если она указана).
  3. Выполняет тело блока with.
  4. Вызывает метод __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 here

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

Когда использовать 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Управление динамическим или условным набором менеджеров контекста

Связанные главы

Практика

Практика
What method does a context manager call when the with block is entered?
What method does a context manager call when the with block is entered?
Практика
What happens when __exit__ returns True?
What happens when __exit__ returns True?
Практика
In a @contextmanager generator, code before the yield statement corresponds to which part of the context manager protocol?
In a @contextmanager generator, code before the yield statement corresponds to which part of the context manager protocol?
Практика
Which contextlib utility suppresses specific exceptions without a try/except block?
Which contextlib utility suppresses specific exceptions without a try/except block?
Was this page helpful?