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
. Потім генератор відновлюється після виходу з блоку. Якщо в блоці виникає необроблена виняткова ситуація, вона повторно створюється всередині генератора в точці, де відбувся вихід. Таким чином, ви можете використовувати операторtry
…except
…finally
, щоб перехопити помилку (якщо така є) або забезпечити виконання певного очищення. Якщо виняток перехоплюється лише для того, щоб зареєструвати його або виконати певну дію (а не повністю його придушити), генератор повинен повторно викликати цей виняток. В іншому випадку менеджер контексту генератора вкаже оператору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 theExitStack
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.
-
coroutine
Приклади та рецепти¶
У цьому розділі описано деякі приклади та рецепти ефективного використання інструментів, наданих 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.
Менеджери контексту для одноразового, багаторазового та повторного входу¶
Більшість контекстних менеджерів написані таким чином, що вони можуть бути ефективно використані в операторі 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