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

This function is a decorator that can be used to define a factory function for with statement context managers, without needing to create a class or separate __enter__() and __exit__() methods.

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

Абстрактним прикладом може бути наступне, щоб забезпечити правильне керування ресурсами:

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-ітератор під час виклику. Цей ітератор має давати рівно одне значення, яке буде прив’язано до цілей у пропозиції as інструкції with, якщо така є.

У точці, де генератор поступається, виконується блок, вкладений у оператор with. Потім генератор відновлюється після виходу з блоку. Якщо в блоці виникає необроблена виняткова ситуація, вона повторно створюється всередині генератора в точці, де відбувся вихід. Таким чином, ви можете використовувати оператор tryexceptfinally, щоб перехопити помилку (якщо така є) або забезпечити виконання певного очищення. Якщо виняток перехоплюється лише для того, щоб зареєструвати його або виконати певну дію (а не повністю його придушити), генератор повинен повторно викликати цей виняток. В іншому випадку менеджер контексту генератора вкаже оператору with, що виняток було оброблено, і виконання буде відновлено оператором, що слідує безпосередньо за оператором with.

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

Змінено в версії 3.2: Використання ContextDecorator.

@contextlib.asynccontextmanager

Подібно до contextmanager(), але створює асинхронний менеджер контексту.

This function is a decorator that can be used to define a factory function for async with statement asynchronous context managers, without needing to create a class or separate __aenter__() and __aexit__() methods. It must be applied to an asynchronous generator function.

Простий приклад:

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

без необхідності явного закриття сторінки. Навіть якщо станеться помилка, page.close() буде викликано під час виходу з блоку with.

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

Нове в версії 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 на інший файл або файлоподібний об’єкт.

Цей інструмент додає гнучкості існуючим функціям або класам, вихід яких підключено до стандартного виводу.

Наприклад, вихід 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 як класу 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]
    # 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

The __enter__() method returns the ExitStack instance, and performs no additional operations.

Кожен екземпляр підтримує стек зареєстрованих зворотних викликів, які викликаються у зворотному порядку, коли екземпляр закривається (явно чи неявно в кінці оператора with). Зауважте, що зворотні виклики не викликаються неявно, коли примірник стеку контексту збирається сміттям.

Ця модель стеку використовується для того, щоб контекстні менеджери, які отримують свої ресурси у своєму методі __init__ (наприклад, файлові об’єкти), могли оброблятися правильно.

Оскільки зареєстровані зворотні виклики викликаються в порядку, зворотному реєстрації, це в кінцевому підсумку поводиться так, ніби декілька вкладених операторів with використовувалися із зареєстрованим набором зворотних викликів. Це навіть поширюється на обробку винятків - якщо внутрішній зворотний виклик пригнічує або замінює виняток, то зовнішнім зворотним викликам будуть передані аргументи на основі цього оновленого стану.

Це API відносно низького рівня, який піклується про деталі правильного розгортання стека зворотних викликів виходу. Він забезпечує відповідну основу для контекстних менеджерів вищого рівня, які маніпулюють стеком виходу у специфічних для програми способах.

Нове в версії 3.3.

enter_context(cm)

Enters a new context manager and adds its __exit__() method to the callback stack. The return value is the result of the context manager’s own __enter__() method.

Ці контекстні менеджери можуть придушувати винятки так само, як вони зазвичай робили б, якщо використовувати безпосередньо як частину оператора with.

push(exit)

Adds a context manager’s __exit__() method to the callback stack.

As __enter__ is not invoked, this method can be used to cover part of an __enter__() implementation with a context manager’s own __exit__() method.

If passed an object that is not a context manager, this method assumes it is a callback with the same signature as a context manager’s __exit__() method and adds it directly to the callback stack.

By returning true values, these callbacks can suppress exceptions the same way context manager __exit__() methods can.

Переданий об’єкт повертається функцією, що дозволяє використовувати цей метод як декоратор функції.

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

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

The close() method is not implemented, aclose() must be used instead.

coroutine enter_async_context(cm)

Similar to enter_context() but expects an asynchronous context manager.

push_async_exit(exit)

Similar to push() but expects either an asynchronous context manager or a coroutine function.

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

Similar to callback() but expects a coroutine function.

coroutine aclose()

Similar to close() but properly handles 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__

As noted in the documentation of ExitStack.push(), this method can be useful in cleaning up an already allocated resource if later steps in the __enter__() implementation fail.

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

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 зі змінною-прапором, яка вказує, чи має бути виконано тіло пропозиції 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()

This allows the intended cleanup up behaviour to be made explicit up front, rather than requiring a separate flag variable.

Якщо певна програма часто використовує цей шаблон, його можна ще більше спростити за допомогою невеликого допоміжного класу:

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

Note that there is one additional limitation when using context managers as function decorators: there’s no way to access the return value of __enter__(). If that value is needed, then it is still necessary to use an explicit with statement.

Дивись також

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 is an example of a reentrant context manager, as are suppress() and redirect_stdout(). Here’s a very simple example of reentrant use:

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