Сообщить об ошибке.

Тип contextmanager, контекстный менеджер

Оператор with в Python поддерживает концепцию контекста среды выполнения, определенного контекстным менеджером. Типичные области применения контекстных менеджеров включают сохранение и восстановление различных типов глобального состояния, блокировку и разблокировку ресурсов, закрытие открытых файлов и т. д.

Содержание:

Синтаксис оператора контекста with:

with EXPRESSION as TARGET:
    SUITE

Семантически эквивалентен:

manager = (EXPRESSION)
enter = type(manager).__enter__
exit = type(manager).__exit__
value = enter(manager)
hit_except = False

try:
    TARGET = value
    SUITE
except:
    hit_except = True
    if not exit(manager, *sys.exc_info()):
        raise
finally:
    if not hit_except:
        exit(manager, None, None, None)

Выражение EXPRESSION, непосредственно следующее за ключевым словом with является "выражением контекста", так как это выражение обеспечивает основной ключ к среде выполнения, которую менеджер контекста устанавливает для продолжительности тела выражения.

Как работает менеджер контекста with:

  1. Выражение контекста (выражение, указанное в EXPRESSION) оценивается для получения менеджера контекста.
  2. Менеджер контекста загружает метод __enter__() для последующего использования.
  3. Менеджер контекста загружает метод __exit__() для последующего использования.
  4. Менеджер контекста вызывает метод __enter__().
  5. Если TARGET была включена в оператор with, то ей присваивается возвращаемое значение из метода __enter__().
    Обратите внимание, что оператор with гарантирует, что если метод __enter__() возвращается без ошибки, то всегда будет вызываться метод __exit__(). Таким образом, если ошибка возникает во время присваивания значения через оператор as, то она будет обрабатываться так же, как и ошибка, возникающая внутри with.
  6. Последовательность команд выполнена.
  7. Вызван метод __exit__(). Если исключение вызвало выход из последовательности команд, то его тип exc_type, значение exc_val и информация о трассировке exc_tb передаются в качестве аргументов __exit__(). В противном случае предоставляется три аргумента None.

Если последовательность команд была завершена из-за исключения, а возвращаемое значение из метода __exit__() было False, то исключение вызывается повторно. Если возвращаемое значение было True, то исключение подавляется и выполнение продолжается с оператора, следующего за оператором with.

Если последовательность команд была завершена по любой причине, кроме исключения, то возвращаемое значение из __exit__() игнорируется, и выполнение продолжается.

При наличии нескольких контекстных менеджеров, они обрабатываются так, как если бы несколько операторов with были вложенными:

with A() as a, B() as b:
    SUITE

# Эквивалентно

with A() as a:
    with B() as b:
        SUITE

С версии Python 3.10 поддерживается использование круглых скобок для написания нескольких диспетчеров. Это позволяет форматировать длинную коллекцию диспетчеров контекста в несколько строк аналогично тому, как это можно с операторами импорта. Например, теперь действительны все эти примеры:

with (CtxManager() as example):
    ...

with (
    CtxManager1(),
    CtxManager2()
):
    ...

with (CtxManager1() as example,
      CtxManager2()):
    ...

with (CtxManager1(),
      CtxManager2() as example):
    ...

with (
    CtxManager1() as example1,
    CtxManager2() as example2
):
    ...

Допускается использовать конечную запятую в конце заключенной группы:

with (
    CtxManager1() as example1,
    CtxManager2() as example2,
    CtxManager3() as example3,
):
    ...

Реализация/протокол менеджера контекста.

Протокол контекстных менеджеров реализован с помощью пары методов, которые позволяют определяемым пользователем классам определять контекст среды выполнения, который вводится до выполнения тела инструкции и завершается при завершении инструкции:

contextmanager.__enter__():

Метод contextmanager.__enter__() вводит контекст среды выполнения и возвращает либо себя, либо другой объект, связанный с контекстом среды выполнения. Значение, возвращаемое этим методом, привязывается к идентификатору в предложении as оператора with, использующего этот контекстный менеджер.

Ярким примером контекстного менеджера, который возвращает себя, является объект file. Файловые объекты возвращают себя из __enter__(), чтобы разрешить использование встроенной функции open() в качестве контекстного выражения в операторе with.

with open('/etc/passwd') as fp:
    for line in fp:
        print line.rstrip()

contextmanager.__exit__(exc_type, exc_val, exc_tb):

Метод contextmanager.__exit__() предоставляет выход из контекста среды выполнения и возвращает логический флаг, указывающий, следует ли подавлять любое возникшее исключение. При возникновении исключения во время выполнения тела оператора with, аргументы содержат тип исключения exc_type, значение exc_val и информацию о трассировке exc_tb. В противном случае все три аргумента - это None.

Если у метода contextmanager.__exit__() установить возвращаемое значение в return True, то это приведет к тому, что оператор with будет подавлять возникающие исключения внутри себя и продолжит выполнение с оператора, непосредственно следующим за оператором with. В противном случае исключение exc_type продолжает распространяться после завершения выполнения этого метода. Исключения, возникающие во время выполнения этого метода, заменят все исключения, возникшие в теле оператора with.

Передаваемое исключение exc_type никогда не следует повторно вызывать явно, вместо этого метод contextmanager.__exit__() должен возвращать return False, чтобы указать, что метод завершился успешно и не хочет подавлять возникшее исключение. Это позволяет коду управления контекстом легко определять, действительно ли метод contextmanager.__exit__() потерпел неудачу.

Пример работы контекстного менеджера:

# файл test_with.py
class Example:
    def __enter__(self):
        print("enter")

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("exit")

Запускаем: python3 -i test_with.py

>>> with Example():
...     print("Python!")

# enter
# Python!
# exit

Также смотрите материал "Создание собственного менеджера контекста".

Полезный контекстный менеджер, который временно изменяет значение переменной среды environ:

# файл test.py
import os

class SetEnv():
    def __init__(self, var_name, new_value):
        # переменные инициализации менеджера контекста
        self.var_name = var_name
        self.new_value = new_value

    def __enter__(self):
        # при входе в менеджер
        # получаем и сохраняем оригинальное значение `environ`
        self.original_value = os.environ.get(self.var_name)
        # применяем новое значение `environ`
        os.environ[self.var_name] = self.new_value

    def __exit__(self, exc_type, exc_val, exc_tb):
        # при выходе из менеджера
        if self.original_value is None:
            # удаляем новое значение `environ`
            del os.environ[self.var_name]
        else:
            # применяем сохраненное (старое) значение `environ`
            os.environ[self.var_name] = self.original_value

Запускаем файл примера в интерактивном режиме: python3 -i test.py

>>> os.environ["USER"]
# 'test' (у вас будет своё значение)

# запускаем менеджер контекста
>>> with SetEnv("USER", "super"):
...     print("USER env from `with`:", os.environ["USER"])

# USER env from `with`: super

# смотрим `environ` после выхода из блока `with`
>>> os.environ["USER"]
# 'test'

Упрощенное создание менеджеров контекста.

Поддержка упрощенного создания менеджеров контекста предоставляется модулем contextlib.

Многие контекстные менеджеры, например, файлы и контексты на основе генераторов будут одноразовыми объектами. После вызова метода __exit__() менеджер контекста больше не будет находиться в работоспособном состоянии (например, файл был закрыт или базовый генератор завершил выполнение).

Необходимость создания нового объекта менеджера контекста для каждого оператора with - это самый простой способ избежать проблем с многопоточным кодом и вложенными операторами, пытающимися использовать один и тот же контекстный менеджер. Не случайно, что все стандартные менеджеры контекста модуля contextlib, поддерживающие повторное использование, происходят из модуля threading и все они уже разработаны для решения проблем, создаваемых потоковым и вложенным использованием.

Это означает, что для сохранения менеджера контекста с определенными аргументами инициализации, которые будут использоваться в нескольких операторах with, как правило, необходимо будет сохранить его в вызываемом объекте с нулевым аргументом, который затем вызывается в выражении контекста каждого оператора, а не кэшировать непосредственно менеджер контекста. Если это ограничение не применяется, это должно быть ясно указано в документации соответствующего контекстного менеджера.

Примеры использования менеджеров контекста:

Шаблон для обеспечения того, что блокировка, полученная в начале блока, освобождается, когда блок закончен:

@contextmanager
def locked(lock):
    lock.acquire()
    try:
        yield
    finally:
        lock.release()

Использование:

with locked(myLock):
    # Здесь код выполняется с удержанным myLock. 
    # lock.release() гарантированно будет выполнен, когда блок
    # будет завершен (даже по необработанному исключению).

Шаблон для открытия файла, который обеспечивает закрытие файла при завершении блока:

@contextmanager
def opened(filename, mode="r"):
    f = open(filename, mode)
    try:
        yield f
    finally:
        f.close()

Использование:

with opened("/etc/passwd") as f:
    for line in f:
        print(line.rstrip())

Шаблон для фиксации или отката транзакции базы данных:

@contextmanager
def transaction(db):
    db.begin()
    try:
        yield None
    except:
        db.rollback()
        raise
    else:
        db.commit()

Временно перенаправить стандартный вывод для однопоточных программ:

@contextmanager
def stdout_redirected(new_stdout):
    save_stdout = sys.stdout
    sys.stdout = new_stdout
    try:
        yield None
    finally:
        sys.stdout = save_stdout

Использование:

with opened(filename, "w") as f:
    with stdout_redirected(f):
        print "Hello world"

Вариант с функцией open(), который также возвращает условие ошибки:

@contextmanager
def opened_w_error(filename, mode="r"):
    try:
        f = open(filename, mode)
    except IOError, err:
        yield None, err
    else:
        try:
            yield f, None
        finally:
            f.close()

Использование:

with opened_w_error("/etc/passwd", "a") as (f, err):
    if err:
        print "IOError:", err
    else:
        f.write("guido::0:0::/:/bin/sh\n")