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

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


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

Коммунальные услуги

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

class contextlib.AbstractContextManager

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

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

class contextlib.AbstractAsyncContextManager

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

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

@contextlib.contextmanager

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

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

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

from contextlib import contextmanager

@contextmanager
def managed_resource(*args, **kwds):
    # Code to acquire resource, e.g.:
    resource = acquire_resource(*args, **kwds)
    try:
        yield resource
    finally:
        # Code to release resource, e.g.:
        release_resource(resource)

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

>>> with managed_resource(timeout=3600) as resource:
...     # Resource is released at the end of this block,
...     # even if code in the block raises an exception

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

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

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

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

@contextlib.asynccontextmanager

Аналогично contextmanager(), но создает asynchronous context manager.

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

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

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:

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 по завершении блока. Это в основном эквивалентно:

from contextlib import contextmanager

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

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

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.

Примечание

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

contextlib.aclosing(thing)

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

from contextlib import asynccontextmanager

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

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

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__, но в остальном ничего не делает. Он предназначен для использования в качестве замены необязательного контекстного менеджера, например:

def myfunction(arg, ignore_exceptions=False):
    if ignore_exceptions:
        # Use suppress to ignore all exceptions.
        cm = contextlib.suppress(Exception)
    else:
        # Do not ignore any exceptions, cm has no effect.
        cm = contextlib.nullcontext()
    with cm:
        # Do something

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

def process_file(file_or_path):
    if isinstance(file_or_path, str):
        # If string, open file
        cm = open(file_or_path)
    else:
        # Caller is responsible for closing file
        cm = nullcontext(file_or_path)

    with cm as file:
        # Perform processing on the file

Он также может быть использован в качестве замены для asynchronous context managers:

async def send_http(session=None):
    if not session:
        # If no http session, create it with aiohttp
        cm = aiohttp.ClientSession()
    else:
        # Caller is responsible for closing the session
        cm = nullcontext(session)

    async with cm as session:
        # Send http requests with session

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

Изменено в версии 3.10: asynchronous context manager добавлена поддержка.

contextlib.suppress(*exceptions)

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

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

Например:

from contextlib import suppress

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

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

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

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

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

Этот контекстный менеджер называется reentrant.

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

contextlib.redirect_stdout(new_target)

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

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

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

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

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

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

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

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

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

Этот контекстный менеджер называется reentrant.

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

contextlib.redirect_stderr(new_target)

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

Этот контекстный менеджер называется reentrant.

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

contextlib.chdir(path)

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

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

Этот контекстный менеджер называется reentrant.

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

class contextlib.ContextDecorator

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

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

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

Пример ContextDecorator:

from contextlib import ContextDecorator

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

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

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

>>> @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

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

def f():
    with cm():
        # Do stuff

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

@cm()
def f():
    # Do stuff

Это дает понять, что cm применяется ко всей функции, а не только к ее фрагменту (и сохранение уровня отступа тоже неплохо).

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

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:

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

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

>>> @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 следующим образом:

with ExitStack() as stack:
    files = [stack.enter_context(open(fname)) for fname in filenames]
    # All opened files will automatically be closed at the end of
    # the with statement, even if attempts to open files later
    # in the list raise an exception

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

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

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

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

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

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

enter_context(cm)

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

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

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

push(exit)

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

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

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

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

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

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

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

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

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

pop_all()

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

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

with ExitStack() as stack:
    files = [stack.enter_context(open(fname)) for fname in filenames]
    # Hold onto the close method, but don't call it yet.
    close_files = stack.pop_all().close
    # If opening any file fails, all previously opened files will be
    # closed automatically. If all files are opened successfully,
    # they will remain open even after the with statement ends.
    # close_files() can then be invoked explicitly to close them all.
close()

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

class contextlib.AsyncExitStack

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

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

coroutine enter_async_context(cm)

Аналогично ExitStack.enter_context(), но ожидает использования асинхронного контекстного менеджера.

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

push_async_exit(exit)

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

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

Аналогично ExitStack.callback(), но ожидает функцию сопрограммы.

coroutine aclose()

Аналогично ExitStack.close(), но правильно обрабатывает доступные.

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

async with AsyncExitStack() as stack:
    connections = [await stack.enter_async_context(get_connection())
        for i in range(5)]
    # All opened connections will automatically be released at the end of
    # the async with statement, even if attempts to open a connection
    # later in the list raise an exception.

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

Примеры и рецепты

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

Поддержка различного числа контекстных менеджеров

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

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)
    # Perform operations that use the acquired resources

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

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

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

stack = ExitStack()
try:
    x = stack.enter_context(cm)
except Exception:
    # handle __enter__ exception
else:
    with stack:
        # Handle normal case

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

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

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

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

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
            # The validation check passed and didn't raise an exception
            # Accordingly, we want to keep the resource, and pass it
            # back to our caller
            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):
        # We don't need to duplicate any of our resource release logic
        self.release_resource()

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

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

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

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

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

from contextlib import ExitStack

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

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

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

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(), чтобы заранее объявить очистку ресурсов:

from contextlib import ExitStack

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

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

Использование контекстного менеджера в качестве декоратора функций

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

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

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)

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

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

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

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

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

См.также

PEP 343 - Оператор «with»

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

Одноразовые, повторно используемые и реентерабельные контекстные менеджеры

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

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

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

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

>>> 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

Управляющие реентерабельным контекстом

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

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

>>> 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 к другому потоку.

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

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

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

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

>>> 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

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

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

>>> 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
Вернуться на верх