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

Источник: Lib/contextlib.py


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

Утилиты

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

class contextlib.AbstractContextManager

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

Added in version 3.6.

class contextlib.AbstractAsyncContextManager

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

Added in version 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 ...')

Added in version 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)

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

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, который закрывает все при выходе из оператора with. Поэтому closing() наиболее полезен для сторонних типов, которые не поддерживают менеджеры контекста. Этот пример приведен исключительно для иллюстрации, поскольку urlopen() обычно используется в менеджере контекста.

contextlib.aclosing(thing)

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

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

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

Added in version 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

Added in version 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.

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

Added in version 3.4.

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

contextlib.redirect_stdout(new_target)

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

Этот инструмент добавляет гибкости существующим функциям или классам, вывод которых жестко привязан к 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.

Added in version 3.4.

contextlib.redirect_stderr(new_target)

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

Этот менеджер контекста имеет значение reentrant.

Added in version 3.5.

contextlib.chdir(path)

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

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

Этот менеджер контекста имеет значение reentrant.

Added in version 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 внутри функции.

Added in version 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

Added in version 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, который заботится о деталях корректного разворачивания стека обратных вызовов выхода. Он обеспечивает подходящую основу для контекстных менеджеров более высокого уровня, которые манипулируют стеком выходов специфическими для приложения способами.

Added in version 3.3.

enter_context(cm)

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

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

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

push(exit)

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

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

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

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

coroutine aclose()

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

Продолжаем пример для 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.

Added in version 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()

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

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

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

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

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

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 - Утверждение «с»

Спецификация, история и примеры для оператора 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

Как видно из примера, повторное использование одного объекта стека в нескольких операторах 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