Перейти к содержимому

contextlib – Утилиты для контекстов оператора with

Исходный код: Lib/contextlib.py


Этот модуль предоставляет утилиты для типичных задач, связанных с оператором with. Дополнительную информацию см. также в Типы менеджеров контекста и Менеджеры контекста оператора with.

UtilitiesУтилиты

Предоставляемые функции и классы:

class contextlib.AbstractContextManager

Абстрактный базовый класс для классов, реализующих __enter__() и __exit__(). Предоставляется реализация по умолчанию для __enter__(), которая возвращает self, в то время как __exit__() является абстрактным методом, который по умолчанию возвращает None. См. также определение Типы менеджеров контекста.

Добавлено в версии 3.6.

class contextlib.AbstractAsyncContextManager

Абстрактный базовый класс для классов, реализующих __aenter__() и __aexit__(). Предоставляется реализация по умолчанию для __aenter__(), которая возвращает self, в то время как __aexit__() является абстрактным методом, который по умолчанию возвращает None. См. также определение Асинхронные менеджеры контекста.

Добавлено в версии 3.7.

@contextlib.contextmanager

Эта функция является декоратором, который можно использовать для определения фабричной функции для менеджеров контекста оператора with, без необходимости создавать класс или отдельные методы __enter__() и __exit__().

Хотя многие объекты изначально поддерживают использование в операторах with, иногда требуется управлять ресурсом, который сам по себе не является менеджером контекста и не реализует метод close() для использования с contextlib.closing.

Абстрактный пример, обеспечивающий корректное управление ресурсами, выглядит следующим образом:

python
from contextlib import contextmanager

@contextmanager
def managed_resource(*args, **kwds):
    # Код для получения ресурса, например:
    resource = acquire_resource(*args, **kwds)
    try:
        yield resource
    finally:
        # Код для освобождения ресурса, например:
        release_resource(resource)

Затем функцию можно использовать так:

python
>>> with managed_resource(timeout=3600) as resource:
...     # Ресурс освобождается в конце этого блока,
...     # даже если код в блоке вызывает исключение

Декорируемая функция при вызове должна возвращать итератор-генератор. Этот итератор должен порождать ровно одно значение, которое будет связано с целями в предложении as оператора with, если таковые имеются.

В точке, где генератор выполняет yield, выполняется блок, вложенный в оператор with. Затем генератор возобновляется после выхода из блока. Если в блоке возникает необработанное исключение, оно повторно возбуждается внутри генератора в точке, где произошел yield. Таким образом, вы можете использовать оператор tryexceptfinally для перехвата ошибки (если она есть) или обеспечения некоторой очистки. Если исключение перехватывается только для его регистрации или выполнения какого-либо действия (а не для полного подавления), генератор должен повторно возбудить это исключение. В противном случае менеджер контекста генератора укажет оператору with, что исключение было обработано, и выполнение продолжится с оператора, следующего непосредственно за оператором with.

contextmanager() использует ContextDecorator, поэтому менеджеры контекста, которые он создаёт, могут использоваться как в качестве декораторов, так и в операторах with. При использовании в качестве декоратора новый экземпляр генератора неявно создаётся при каждом вызове функции (это позволяет в противном случае «одноразовым» менеджерам контекста, созданным contextmanager(), удовлетворять требованию, чтобы менеджеры контекста поддерживали множественные вызовы для использования в качестве декораторов).

Изменено в версии 3.2: Использование ContextDecorator.

@contextlib.asynccontextmanager

Аналогично contextmanager(), но создаёт асинхронный менеджер контекста.

Эта функция является декоратором, который можно использовать для определения фабричной функции для асинхронных менеджеров контекста оператора async with, без необходимости создавать класс или отдельные методы __aenter__() и __aexit__(). Она должна применяться к асинхронной функции-генератору.

Простой пример:

python
from contextlib import asynccontextmanager

@asynccontextmanager
async def get_connection():
    conn = await acquire_db_connection()
    try:
        yield conn
    finally:
        await release_db_connection(conn)

async def get_all_users():
    async with get_connection() as conn:
        return conn.query('SELECT ...')

Добавлено в версии 3.7.

Менеджеры контекста, определённые с помощью asynccontextmanager(), могут использоваться либо как декораторы, либо с операторами async with:

python
import time
from contextlib import asynccontextmanager

@asynccontextmanager
async def timeit():
    now = time.monotonic()
    try:
        yield
    finally:
        print(f'it took {time.monotonic() - now}s to run')

@timeit()
async def main():
    # ... async code ...

При использовании в качестве декоратора новый экземпляр генератора неявно создаётся при каждом вызове функции. Это позволяет в противном случае «одноразовым» менеджерам контекста, созданным asynccontextmanager(), удовлетворять требованию, что менеджеры контекста поддерживают множественные вызовы для использования в качестве декораторов.

Изменено в версии 3.10: Асинхронные менеджеры контекста, созданные с помощью asynccontextmanager(), могут использоваться в качестве декораторов.

contextlib.closing(thing)

Возвращает менеджер контекста, который закрывает thing по завершении блока. В основном эквивалентно:

python
from contextlib import contextmanager

@contextmanager
def closing(thing):
    try:
        yield thing
    finally:
        thing.close()

И позволяет писать код так:

python
from contextlib import closing
from urllib.request import urlopen

with closing(urlopen('https://www.python.org')) as page:
    for line in page:
        print(line)

без необходимости явно закрывать page. Даже если возникает ошибка, page.close() будет вызван при выходе из блока with.

Примечание

Большинство типов, управляющих ресурсами, поддерживают протокол менеджера контекста, который закрывает thing при выходе из оператора with. Таким образом, closing() наиболее полезен для сторонних типов, которые не поддерживают менеджеры контекста. Этот пример приведён исключительно в иллюстративных целях, так как urlopen() обычно используется в менеджере контекста.

contextlib.aclosing(thing)

Возвращает асинхронный менеджер контекста, который вызывает метод aclose() объекта thing по завершении блока. В основном эквивалентно:

python
from contextlib import asynccontextmanager

@asynccontextmanager
async def aclosing(thing):
    try:
        yield thing
    finally:
        await thing.aclose()

Важно, что aclosing() поддерживает детерминированную очистку асинхронных генераторов, когда они досрочно завершаются по break или исключению. Например:

python
from contextlib import aclosing

async with aclosing(my_generator()) as values:
    async for value in values:
        if value == 42:
            break

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

Добавлено в версии 3.10.

contextlib.nullcontext(enter_result=None)

Возвращает менеджер контекста, который возвращает enter_result из __enter__(), но в остальном ничего не делает. Он предназначен для использования в качестве замены опционального менеджера контекста, например:

python
def myfunction(arg, ignore_exceptions=False):
    if ignore_exceptions:
        # Используйте suppress, чтобы игнорировать все исключения.
        cm = contextlib.suppress(Exception)
    else:
        # Не игнорируйте никакие исключения, cm не действует.
        cm = contextlib.nullcontext()
    with cm:
        # Сделайте что-нибудь

Пример использования enter_result:

python
def process_file(file_or_path):
    if isinstance(file_or_path, str):
        # Если строка, открыть файл
        cm = open(file_or_path)
    else:
        # Вызывающий отвечает за закрытие файла
        cm = nullcontext(file_or_path)

    with cm as file:
        # Выполнить обработку файла

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

python
async def send_http(session=None):
    if not session:
        # Если нет http-сессии, создать её с помощью aiohttp
        cm = aiohttp.ClientSession()
    else:
        # Вызывающий отвечает за закрытие сессии
        cm = nullcontext(session)

    async with cm as session:
        # Отправлять HTTP-запросы с сессией

Добавлено в версии 3.7.

Изменено в версии 3.10: добавлена поддержка асинхронного менеджера контекста.

contextlib.suppress(*exceptions)

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

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

Например:

python
from contextlib import suppress

with suppress(FileNotFoundError):
    os.remove('somefile.tmp')

with suppress(FileNotFoundError):
    os.remove('someotherfile.tmp')

Этот код эквивалентен:

python
try:
    os.remove('somefile.tmp')
except FileNotFoundError:
    pass

try:
    os.remove('someotherfile.tmp')
except FileNotFoundError:
    pass

Этот менеджер контекста является реентерабельным.

Если код внутри блока with вызывает BaseExceptionGroup, подавленные исключения удаляются из группы. Любые исключения группы, которые не были подавлены, повторно возбуждаются в новой группе, которая создаётся с использованием метода derive() исходной группы.

Добавлено в версии 3.4.

Изменено в версии 3.12: suppress теперь поддерживает подавление исключений, возникших как часть BaseExceptionGroup.

contextlib.redirect_stdout(new_target)

Менеджер контекста для временного перенаправления sys.stdout в другой файловый объект.

Этот инструмент добавляет гибкость существующим функциям или классам, чей вывод жестко привязан к sys.stdout.

Например, вывод help() обычно отправляется в sys.stdout. Можно захватить этот вывод в строку, перенаправив вывод в объект io.StringIO. Замещающий поток возвращается из метода __enter__() и, таким образом, доступен как цель оператора with:

python
with redirect_stdout(io.StringIO()) as f:
    help(pow)
s = f.getvalue()

Чтобы отправить вывод help() в файл на диске, перенаправьте вывод в обычный файл:

python
with open('help.txt', 'w') as f:
    with redirect_stdout(f):
        help(pow)

Чтобы отправить вывод help() в sys.stderr:

python
with redirect_stdout(sys.stderr):
    help(pow)

Обратите внимание, что глобальный побочный эффект на sys.stdout означает, что этот менеджер контекста не подходит для использования в библиотечном коде и большинстве многопоточных приложений. Он также не влияет на вывод подпроцессов. Тем не менее, это все еще полезный подход для многих служебных скриптов.

Этот менеджер контекста является реентерабельным.

Добавлено в версии 3.4.

contextlib.redirect_stderr(new_target)

Аналогично redirect_stdout(), но перенаправляет глобальный sys.stderr в другой файловый объект.

Этот менеджер контекста является реентерабельным.

Добавлено в версии 3.5.

contextlib.chdir(path)

Менеджер контекста, не безопасный для параллельного использования, для изменения текущего рабочего каталога. Поскольку это изменяет глобальное состояние – рабочий каталог, он не подходит для использования в большинстве многопоточных или асинхронных контекстов. Он также не подходит для большинства нелинейных выполнений кода, таких как генераторы, где выполнение программы временно приостанавливается – если только это не нужно явно, не следует выполнять yield, когда этот менеджер контекста активен.

Это простая обёртка вокруг chdir(), она изменяет текущий рабочий каталог при входе и восстанавливает старый при выходе.

Этот менеджер контекста является реентерабельным.

Добавлено в версии 3.11.

class contextlib.ContextDecorator

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

Менеджеры контекста, наследуемые от ContextDecorator, должны реализовывать __enter__() и __exit__() как обычно. __exit__ сохраняет свою опциональную обработку исключений, даже когда используется как декоратор.

ContextDecorator используется contextmanager(), поэтому эта функциональность предоставляется автоматически.

Пример ContextDecorator:

python
from contextlib import ContextDecorator

class mycontext(ContextDecorator):
    def __enter__(self):
        print('Starting')
        return self

    def __exit__(self, *exc):
        print('Finishing')
        return False

Затем класс можно использовать следующим образом:

python
>>> @mycontext()
... def function():
...     print('The bit in the middle')
...
>>> function()
Starting
The bit in the middle
Finishing

>>> with mycontext():
...     print('The bit in the middle')
...
Starting
The bit in the middle
Finishing

Это изменение – всего лишь синтаксический сахар для любой конструкции следующего вида:

python
def f():
    with cm():
        # Выполнять действия

ContextDecorator позволяет вместо этого писать:

python
@cm()
def f():
    # Выполнять действия

Это проясняет, что cm применяется ко всей функции, а не только к её части (а также экономия уровня отступа – приятный бонус).

Существующие менеджеры контекста, у которых уже есть базовый класс, могут быть расширены путем использования ContextDecorator в качестве миксин-класса:

python
from contextlib import ContextDecorator

class mycontext(ContextBaseClass, ContextDecorator):
    def __enter__(self):
        return self

    def __exit__(self, *exc):
        return False

Примечание

Поскольку декорированная функция должна иметь возможность вызываться несколько раз, базовый менеджер контекста должен поддерживать использование в нескольких операторах with. Если это не так, то следует использовать исходную конструкцию с явным оператором with внутри функции.

Добавлено в версии 3.2.

class contextlib.AsyncContextDecorator

Аналогично ContextDecorator, но только для асинхронных функций.

Пример AsyncContextDecorator:

python
from asyncio import run
from contextlib import AsyncContextDecorator

class mycontext(AsyncContextDecorator):
    async def __aenter__(self):
        print('Starting')
        return self

    async def __aexit__(self, *exc):
        print('Finishing')
        return False

Затем класс можно использовать следующим образом:

python
>>> @mycontext()
... async def function():
...     print('The bit in the middle')
...
>>> run(function())
Starting
The bit in the middle
Finishing

>>> async def function():
...    async with mycontext():
...         print('The bit in the middle')
...
>>> run(function())
Starting
The bit in the middle
Finishing

Добавлено в версии 3.10.

class contextlib.ExitStack

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

Например, набор файлов можно легко обработать в одном операторе with следующим образом:

python
with ExitStack() as stack:
    files = [stack.enter_context(open(fname)) for fname in filenames]
    # Все открытые файлы будут автоматически закрыты в конце
    # оператора with, даже если попытки открыть файлы позже
    # в списке вызовут исключение

Метод __enter__() возвращает экземпляр ExitStack и не выполняет никаких дополнительных операций.

Каждый экземпляр поддерживает стек зарегистрированных колбэков, которые вызываются в обратном порядке при закрытии экземпляра (явно или неявно в конце оператора with). Обратите внимание, что колбэки не вызываются неявно при сборке мусора экземпляра стека контекста.

Эта модель стека используется для того, чтобы менеджеры контекста, которые получают свои ресурсы в своём методе __init__ (например, файловые объекты), могли обрабатываться корректно.

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

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

Добавлено в версии 3.3.

enter_context(cm)

Входит в новый менеджер контекста и добавляет его метод __exit__() в стек колбэков. Возвращаемое значение – результат собственного метода __enter__() менеджера контекста.

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

Изменено в версии 3.11: Возбуждает TypeError вместо AttributeError, если cm не является менеджером контекста.

Изменено в версии 3.15: Добавлена поддержка произвольных дескрипторов __enter__() и __exit__().

push(exit)

Добавляет метод __exit__() менеджера контекста в стек колбэков.

Поскольку __enter__ не вызывается, этот метод можно использовать для покрытия части реализации __enter__() собственным методом __exit__() менеджера контекста.

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

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

Переданный объект возвращается из функции, что позволяет использовать этот метод в качестве декоратора функции.

Изменено в версии 3.15: Добавлена поддержка произвольных дескрипторов __exit__().

callback(callback, /, *args, **kwds)

Принимает произвольную функцию колбэка и аргументы и добавляет её в стек колбэков.

В отличие от других методов, колбэки, добавленные таким образом, не могут подавлять исключения (поскольку им никогда не передаются детали исключения).

Переданный колбэк возвращается из функции, что позволяет использовать этот метод в качестве декоратора функции.

pop_all()

Передаёт стек колбэков новому экземпляру ExitStack и возвращает его. Никакие колбэки не вызываются этой операцией – вместо этого они будут вызываться при закрытии нового стека (явно или неявно в конце оператора with).

Например, группу файлов можно открыть как операцию «всё или ничего» следующим образом:

python
with ExitStack() as stack:
    files = [stack.enter_context(open(fname)) for fname in filenames]
    # Сохранить метод close, но пока не вызывать его.
    close_files = stack.pop_all().close
    # Если открытие любого файла завершится неудачей, все ранее открытые файлы будут
    # закрыты автоматически. Если все файлы открыты успешно,
    # они останутся открытыми даже после завершения оператора with.
    # close_files() затем можно вызвать явно, чтобы закрыть их все.
close()

Немедленно разматывает стек колбэков, вызывая колбэки в порядке, обратном регистрации. Для любых зарегистрированных менеджеров контекста и выходных колбэков переданные аргументы будут указывать на то, что исключения не возникло.

class contextlib.AsyncExitStack

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

Метод close() не реализован; вместо него необходимо использовать aclose().

async enter_async_context(cm)

Подобно ExitStack.enter_context(), но ожидает асинхронный контекстный менеджер.

Изменено в версии 3.11: Возбуждает TypeError вместо AttributeError, если cm не является асинхронным контекстным менеджером.

Изменено в версии 3.15: Добавлена поддержка произвольных дескрипторов __aenter__() и __aexit__().

push_async_exit(exit)

Подобно ExitStack.push(), но ожидает либо асинхронный контекстный менеджер, либо корутинную функцию.

Изменено в версии 3.15: Добавлена поддержка произвольных дескрипторов __aexit__().

push_async_callback(callback, /, *args, **kwds)

Подобно ExitStack.callback(), но ожидает корутинную функцию.

async aclose()

Подобно ExitStack.close(), но корректно обрабатывает ожидаемые объекты.

Продолжая пример для asynccontextmanager():

python
async with AsyncExitStack() as stack:
    connections = [await stack.enter_async_context(get_connection())
        for i in range(5)]
    # Все открытые соединения будут автоматически освобождены в конце
    # оператора async with, даже если попытки открыть соединение
    # позже в списке вызовут исключение.

Добавлено в версии 3.7.

Examples and RecipesПримеры и рецепты

В этом разделе описаны некоторые примеры и рецепты эффективного использования инструментов, предоставляемых contextlib.

Supporting a variable number of context managersПоддержка переменного количества контекстных менеджеров

Основной вариант использования ExitStack – это тот, что приведен в документации класса: поддержка переменного количества контекстных менеджеров и других операций очистки в одном операторе with. Изменчивость может быть обусловлена тем, что количество необходимых контекстных менеджеров определяется пользовательским вводом (например, открытие указанной пользователем коллекции файлов) или тем, что некоторые контекстные менеджеры являются необязательными:

python
with ExitStack() as stack:
    for resource in resources:
        stack.enter_context(resource)
    if need_special_resource():
        special = acquire_special_resource()
        stack.callback(release_special_resource, special)
    # Выполнять операции, использующие полученные ресурсы

Как показано, ExitStack также значительно упрощает использование операторов with для управления произвольными ресурсами, которые изначально не поддерживают протокол управления контекстом.

Перехват исключений из методов __enter__

Иногда бывает желательно перехватывать исключения из реализации метода __enter__(), не перехватывая случайно исключения из тела оператора with или метода __exit__() контекстного менеджера. Используя ExitStack, можно немного разделить шаги протокола управления контекстом, чтобы это стало возможным:

python
stack = ExitStack()
try:
    x = stack.enter_context(cm)
except Exception:
    # обработать исключение __enter__
else:
    with stack:
        # Обработать обычный случай

Фактическая необходимость делать это, вероятно, указывает на то, что нижележащий API должен предоставлять прямой интерфейс управления ресурсами для использования с операторами try/except/finally, но не все API хорошо спроектированы в этом отношении. Когда контекстный менеджер является единственным предоставленным API управления ресурсами, тогда ExitStack может облегчить обработку различных ситуаций, которые невозможно обработать напрямую в операторе with.

Очистка в реализации __enter__

Как отмечено в документации ExitStack.push(), этот метод может быть полезен для очистки уже выделенного ресурса, если последующие шаги в реализации __enter__() завершатся неудачей.

Вот пример того, как это сделать для контекстного менеджера, который принимает функции захвата и освобождения ресурса, а также опциональную функцию проверки, и сопоставляет их с протоколом управления контекстом:

python
from contextlib import contextmanager, AbstractContextManager, ExitStack

class ResourceManager(AbstractContextManager):

    def __init__(self, acquire_resource, release_resource, check_resource_ok=None):
        self.acquire_resource = acquire_resource
        self.release_resource = release_resource
        if check_resource_ok is None:
            def check_resource_ok(resource):
                return True
        self.check_resource_ok = check_resource_ok

    @contextmanager
    def _cleanup_on_error(self):
        with ExitStack() as stack:
            stack.push(self)
            yield
            # Проверка валидации прошла успешно и не вызвала исключения
            # Соответственно, мы хотим сохранить ресурс и передать его
            # обратно вызывающему коду
            stack.pop_all()

    def __enter__(self):
        resource = self.acquire_resource()
        with self._cleanup_on_error():
            if not self.check_resource_ok(resource):
                msg = "Failed validation for {!r}"
                raise RuntimeError(msg.format(resource))
        return resource

    def __exit__(self, *exc_details):
        # Нам не нужно дублировать логику освобождения ресурсов
        self.release_resource()

Замена любого использования try-finally и флаговых переменных

Шаблон, который иногда можно увидеть, – это оператор try-finally с флаговой переменной, указывающей, следует ли выполнять тело предложения finally. В своей простейшей форме (которую еще нельзя обработать простым использованием предложения except вместо этого), это выглядит примерно так:

python
cleanup_needed = True
try:
    result = perform_operation()
    if result:
        cleanup_needed = False
finally:
    if cleanup_needed:
        cleanup_resources()

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

ExitStack позволяет вместо этого зарегистрировать колбэк для выполнения в конце оператора with, а затем позже решить пропустить выполнение этого колбэка:

python
from contextlib import ExitStack

with ExitStack() as stack:
    stack.callback(cleanup_resources)
    result = perform_operation()
    if result:
        stack.pop_all()

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

Если конкретное приложение часто использует этот шаблон, его можно еще больше упростить с помощью небольшого вспомогательного класса:

python
from contextlib import ExitStack

class Callback(ExitStack):
    def __init__(self, callback, /, *args, **kwds):
        super().__init__()
        self.callback(callback, *args, **kwds)

    def cancel(self):
        self.pop_all()

with Callback(cleanup_resources) as cb:
    result = perform_operation()
    if result:
        cb.cancel()

Если очистка ресурса еще не аккуратно упакована в отдельную функцию, то все равно можно использовать форму декоратора ExitStack.callback(), чтобы объявить очистку ресурса заранее:

python
from contextlib import ExitStack

with ExitStack() as stack:
    @stack.callback
    def cleanup_resources():
        ...
    result = perform_operation()
    if result:
        stack.pop_all()

Из-за того, как работает протокол декоратора, функция колбэка, объявленная таким образом, не может принимать никаких параметров. Вместо этого любые ресурсы, подлежащие освобождению, должны быть доступны как переменные замыкания.

Using a context manager as a function decoratorИспользование контекстного менеджера в качестве декоратора функции

ContextDecorator делает возможным использование контекстного менеджера как в обычном операторе with, так и в качестве декоратора функции.

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

python
from contextlib import ContextDecorator
import logging

logging.basicConfig(level=logging.INFO)

class track_entry_and_exit(ContextDecorator):
    def __init__(self, name):
        self.name = name

    def __enter__(self):
        logging.info('Entering: %s', self.name)

    def __exit__(self, exc_type, exc, exc_tb):
        logging.info('Exiting: %s', self.name)

Экземпляры этого класса могут использоваться как в качестве менеджера контекста:

python
with track_entry_and_exit('widget loader'):
    print('Some time consuming activity goes here')
    load_widget()

А также в качестве декоратора функции:

python
@track_entry_and_exit('widget loader')
def activity():
    print('Some time consuming activity goes here')
    load_widget()

Обратите внимание, что при использовании менеджеров контекста в качестве декораторов функций существует одно дополнительное ограничение: нет способа получить доступ к возвращаемому значению __enter__(). Если это значение необходимо, то всё равно придётся использовать явный оператор with.

Смотрите также

PEP 343 – оператор «with»

Спецификация, предыстория и примеры для оператора with языка Python.

Single use, reusable and reentrant context managersОдноразовые, многоразовые и реентерабельные менеджеры контекста

Большинство менеджеров контекста написаны таким образом, что их можно эффективно использовать только один раз в операторе with. Эти одноразовые менеджеры контекста должны создаваться заново каждый раз при использовании – попытка использовать их второй раз вызовет исключение или приведёт к некорректной работе.

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

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

Менеджеры контекста, созданные с помощью contextmanager(), также являются одноразовыми, и при попытке использовать их второй раз они сообщат об ошибке, связанной с тем, что нижележащий генератор не возвращает управление (не делает yield):

python
>>> from contextlib import contextmanager
>>> @contextmanager
... def singleuse():
...     print("Before")
...     yield
...     print("After")
...
>>> cm = singleuse()
>>> with cm:
...     pass
...
Before
After
>>> with cm:
...     pass
...
Traceback (most recent call last):
    ...
RuntimeError: generator didn't yield

Reentrant context managersРеентерабельные менеджеры контекста

Более сложные менеджеры контекста могут быть «реентерабельными». Такие менеджеры контекста можно не только использовать в нескольких операторах with, но и применять внутри оператора with, который уже использует тот же менеджер контекста.

threading.RLock является примером реентерабельного менеджера контекста, как и suppress(), redirect_stdout() и chdir(). Вот очень простой пример реентерабельного использования:

python
>>> from contextlib import redirect_stdout
>>> from io import StringIO
>>> stream = StringIO()
>>> write_to_stream = redirect_stdout(stream)
>>> with write_to_stream:
...     print("This is written to the stream rather than stdout")
...     with write_to_stream:
...         print("This is also written to the stream")
...
>>> print("This is written directly to stdout")
This is written directly to stdout
>>> print(stream.getvalue())
This is written to the stream rather than stdout
This is also written to the stream

Реальные примеры реентерабельности чаще включают несколько функций, вызывающих друг друга, и поэтому намного сложнее этого примера.

Также обратите внимание, что реентерабельность – не то же самое, что потокобезопасность. redirect_stdout(), например, определённо не является потокобезопасным, так как он вносит глобальное изменение в состояние системы, связывая sys.stdout с другим потоком данных.

Reusable context managersМногоразовые менеджеры контекста

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

threading.Lock является примером многоразового, но не реентерабельного менеджера контекста (для реентерабельной блокировки необходимо использовать threading.RLock).

Другим примером многоразового, но не реентерабельного менеджера контекста является ExitStack, так как он вызывает все зарегистрированные на данный момент колбэки при выходе из любого оператора with, независимо от того, где эти колбэки были добавлены:

python
>>> from contextlib import ExitStack
>>> stack = ExitStack()
>>> with stack:
...     stack.callback(print, "Callback: from first context")
...     print("Leaving first context")
...
Leaving first context
Callback: from first context
>>> with stack:
...     stack.callback(print, "Callback: from second context")
...     print("Leaving second context")
...
Leaving second context
Callback: from second context
>>> with stack:
...     stack.callback(print, "Callback: from outer context")
...     with stack:
...         stack.callback(print, "Callback: from inner context")
...         print("Leaving inner context")
...     print("Leaving outer context")
...
Leaving inner context
Callback: from inner context
Callback: from outer context
Leaving outer context

Как показывает вывод примера, повторное использование одного объекта стека в нескольких операторах with работает корректно, но попытка вложить их приведёт к очистке стека в конце самого внутреннего оператора with, что вряд ли является желаемым поведением.

Использование отдельных экземпляров ExitStack вместо повторного использования одного экземпляра позволяет избежать этой проблемы:

python
>>> from contextlib import ExitStack
>>> with ExitStack() as outer_stack:
...     outer_stack.callback(print, "Callback: from outer context")
...     with ExitStack() as inner_stack:
...         inner_stack.callback(print, "Callback: from inner context")
...         print("Leaving inner context")
...     print("Leaving outer context")
...
Leaving inner context
Callback: from inner context
Leaving outer context
Callback: from outer context