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)

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

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.

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 на другой файл или файлоподобный объект.

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

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

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

enter_context(cm)

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

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

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)

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

push_async_exit(exit)

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

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

Аналогично callback(), но ожидает функцию coroutine.

coroutine aclose()

Аналогичен 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.

Добавлено в версии 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(). Вот очень простой пример реентерабельного использования:

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

Многократно используемые менеджеры контекста

От одноразовых и реентерабельных контекстных менеджеров отличаются «многоразовые» контекстные менеджеры (или, чтобы быть полностью ясным, «многоразовые, но не реентерабельные» контекстные менеджеры, поскольку реентерабельные контекстные менеджеры также являются многоразовыми). Эти контекстные менеджеры поддерживают многократное использование, но будут работать некорректно, если конкретный экземпляр контекстного менеджера уже использовался в операторе containing 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
Вернуться на верх