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__() методы.

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

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

В точке, где возвращается генератор, выполняется блок, вложенный в оператор with. Затем генератор возобновляется после выхода из блока. Если в блоке происходит необработанное исключение, оно повторно поднимается в генератор в пункте, где произошел yield. Таким образом, можно использовать try …:keyword:exceptfinally оператор для треппинга ошибки (при её наличии) или проверки выполнения очистки. Если исключение захватывается только для того, чтобы записать его в журнал или выполнить какое-либо действие (а не полностью подавить его), то генератор должен повторно создать это исключение. В противном случае контекстый менеджер генератора будет указывать оператору 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)

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

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.python.org')) as page:
    for line in page:
        print(line)

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

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

Это контекстый менеджер reentrant.

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

Это контекстый менеджер 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():
        # Делать что-то...

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

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

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

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

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(), но правильно обрабатывает await.

Продолжение примера для 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('Entering: %s', self.name)

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

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

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

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

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

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

См.также

PEP 343 - Оператор «with»
Спецификация, бэкграунд и примеры для Python оператора with.

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

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

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

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

Менеджеры контекста, созданные с помощью 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 в другом потоке.

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

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

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

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

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

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

Использование отдельной 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