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

Тип 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__() потерпел неудачу.

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

Поддержка упрощенного создания менеджеров контекста предоставляется модулем 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")