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

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


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

Утилиты

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

class contextlib.AbstractContextManager

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

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

class contextlib.AbstractAsyncContextManager

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

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

@contextlib.contextmanager

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

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

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

from contextlib import contextmanager

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

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

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

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

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

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

@contextlib.asynccontextmanager

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

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

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

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.

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('http://www.example.int')) as page:
    for line in page:
        print(line)

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

contextlib.nullcontext(enter_result=None)

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

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

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

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

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

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

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

Данный менеджер контекста — реентерабельный.

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

contextlib.redirect_stdout(new_target)

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

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

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

f = io.StringIO()
with redirect_stdout(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 означает, что этот менеджер контекста не подходит для использования в коде библиотеки и в большинстве многопоточных приложений. Также не влияет на вывод подпроцессов. Тем не менее, это всё ещё полезный подход для многих служебных скриптов.

Данный менеджер контекста — реентерабелен.

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

contextlib.redirect_stderr(new_target)

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

Данный менеджер контекста — реентерабелен.

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

class contextlib.ContextDecorator

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

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

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

Пример ContextDecorator:

from contextlib import ContextDecorator

class mycontext(ContextDecorator):
    def __enter__(self):
        print('Запуск')
        return self

    def __exit__(self, *exc):
        print('Завершение')
        return False

>>> @mycontext()
... def function():
...     print('Кусочек посередине')
...
>>> function()
Запуск
Кусочек посередине
Завершение

>>> with mycontext():
...     print('Кусочек посередине')
...
Запуск
Кусочек посередине
Завершение

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

def f():
    with cm():
        # Делать что-то...

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

@cm()
def f():
    # Делать что-то...

Это даёт понять, что 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.ExitStack

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

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

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

Каждый экземпляр поддерживает стек зарегистрированных обратных вызовов, которые вызываются в обратном порядке при закрытии экземпляра (явно или неявно в конце оператора 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]
    # Придерживайтесь метода close, но пока не вызывайте его.
    close_files = stack.pop_all().close
    # Если открытие любого файла завершится неудачно, то все ранее открытые файлы будут удалены.
    # закрывать автоматически. Если все файлы открыты успешно,
    # они будут оставаться открытыми даже после того, как закончится оператор with.
    # close_files() затем можно вызвать вызов явно, чтобы закрыть их все.
close()

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

class contextlib.AsyncExitStack

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

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

enter_async_context(cm)

Подобно enter_context(), но предполагает наличие асинхронного менеджера контекста.

push_async_exit(exit)

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

push_async_callback(callback, *args, **kwds)

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

aclose()

Аналогично close(), но правильно обрабатывает ожидаемые объекты.

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

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

Добавлено в версии 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)
    # Выполнение операций с использованием захваченных ресурсов

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

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

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

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

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

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

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

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

from contextlib import contextmanager, AbstractContextManager, ExitStack

class ResourceManager(AbstractContextManager):

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

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

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

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

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

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

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(Callback, self).__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('Вход: %s', self.name)

    def __exit__(self, exc_type, exc, exc_tb):
        logging.info('Выход: %s', self.name)

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

with track_entry_and_exit('загрузчик виджетов'):
    print('Здесь происходит некоторая трудоемкая деятельность')
    load_widget()

А также как декоратор функций:

@track_entry_and_exit('загрузчик виджетов')
def activity():
    print('Здесь происходит некоторая трудоемкая деятельность')
    load_widget()

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

См.также

PEP 343 — Оператор «with»
Спецификация, предыстория и примеры для Python оператора with.

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

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

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

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

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

>>> from contextlib import contextmanager
>>> @contextmanager
... def singleuse():
...     print("До")
...     yield
...     print("После")
...
>>> cm = singleuse()
>>> with cm:
...     pass
...
До
После
>>> 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("Это записывается в поток, а не в stdout")
...     with write_to_stream:
...         print("Это также записывается в поток")
...
>>> print("Это записывается непосредственно в stdout")
Это записывается непосредственно в stdout
>>> print(stream.getvalue())
Это записывается в поток, а не в stdout
Это также записывается в поток

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

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

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

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

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

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

>>> from contextlib import ExitStack
>>> stack = ExitStack()
>>> with stack:
...     stack.callback(print, "Обратный вызов: из первого контекста")
...     print("Выход из первого контекста")
...
Выход из первого контекста
Обратный вызов: из первого контекста
>>> with stack:
...     stack.callback(print, "Обратный вызов: из второго контекста")
...     print("Выход из второго контекста")
...
Выход из второго контекста
Обратный вызов: из второго контекста
>>> with stack:
...     stack.callback(print, "Обратный вызов: из внешнего контекста")
...     with stack:
...         stack.callback(print, "Обратный вызов: из внутреннего контекста")
...         print("Выход из внутреннего контекста")
...     print("Выход из внешнего контекста")
...
Выход из внутреннего контекста
Обратный вызов: из внутреннего контекста
Обратный вызов: из внешнего контекста
Выход из внешнего контекста

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

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

>>> from contextlib import ExitStack
>>> with ExitStack() as outer_stack:
...     outer_stack.callback(print, "Обратный вызов: из внешнего контекста")
...     with ExitStack() as inner_stack:
...         inner_stack.callback(print, "Обратный вызов: из внутреннего контекста")
...         print("Выход из внутреннего контекста")
...     print("Выход из внешнего контекста")
...
Выход из внутреннего контекста
Обратный вызов: из внутреннего контекста
Выход из внешнего контекста
Обратный вызов: из внешнего контекста