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): # 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
被装饰的函数在被调用时必须返回一个 生成器-迭代器。此迭代器必须只产生一个值,该值将绑定到
with
语句的as
子句中的目标(如果有)。在生成器产生值的位置,执行嵌套在
with
语句中的块。在块退出后,生成器将恢复。如果在块中发生未处理的异常,则会在生成器中发生 yield 的位置重新引发该异常。因此,可以使用try
...except
...finally
语句来捕获错误(如果有),或确保进行一些清理。如果捕获异常只是为了记录它或执行一些操作(而不是完全抑制它),则生成器必须重新引发该异常。否则,生成器上下文管理器将向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 版本新增。
使用
asynccontextmanager()
定义的上下文管理器可以用作装饰器或与async with
语句一起使用import time from contextlib import asynccontextmanager @asynccontextmanager async def timeit(): now = time.monotonic() try: yield finally: print(f'it took {time.monotonic() - now}s to run') @timeit() async def main(): # ... async code ...
当用作装饰器时,会在每次函数调用时隐式创建一个新的生成器实例。这允许
asynccontextmanager()
创建的原本 “一次性” 的上下文管理器满足上下文管理器支持多次调用的要求,以便用作装饰器。在 3.10 版本中更改: 使用
asynccontextmanager()
创建的异步上下文管理器可以用作装饰器。
- contextlib.closing(thing)¶
返回一个上下文管理器,在代码块执行完毕后关闭 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://pythonlang.cn')) as page: for line in page: print(line)
而无需显式关闭
page
。即使发生错误,当with
代码块退出时,也会调用page.close()
。
- contextlib.aclosing(thing)¶
返回一个异步上下文管理器,该管理器在代码块完成时调用 thing 的
aclose()
方法。这基本上等同于:from contextlib import asynccontextmanager @asynccontextmanager async def aclosing(thing): try: yield thing finally: await thing.aclose()
重要的是,当异步生成器由于
break
或异常而提前退出时,aclosing()
支持对它们进行确定性的清理。例如:from contextlib import aclosing async with aclosing(my_generator()) as values: async for value in values: if value == 42: break
此模式确保生成器的异步退出代码在其迭代的同一上下文中执行(以便异常和上下文变量按预期工作,并且退出代码不会在它所依赖的某些任务的生命周期之后运行)。
3.10 版本新增。
- contextlib.nullcontext(enter_result=None)¶
返回一个上下文管理器,它从
__enter__
返回 enter_result,但其他情况下什么也不做。它旨在用作可选上下文管理器的替代,例如: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
它也可以用作 异步上下文管理器 的替代:
async def send_http(session=None): if not session: # If no http session, create it with aiohttp cm = aiohttp.ClientSession() else: # Caller is responsible for closing the session cm = nullcontext(session) async with cm as session: # Send http requests with session
3.7 版本新增。
在 3.10 版本中更改: 添加了对 异步上下文管理器 的支持。
- 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
此上下文管理器是可重入的。
如果
with
代码块中的代码引发BaseExceptionGroup
,则将从组中删除被抑制的异常。组中未被抑制的任何异常将使用原始组的derive()
方法创建一个新组,并在新组中重新引发。3.4 版本新增。
在 3.12 版本中更改:
suppress
现在支持抑制作为BaseExceptionGroup
的一部分引发的异常。
- contextlib.redirect_stdout(new_target)¶
用于将
sys.stdout
临时重定向到另一个文件或类文件对象的上下文管理器。此工具为输出硬连线到 stdout 的现有函数或类增加了灵活性。
例如,
help()
的输出通常会发送到 sys.stdout。您可以通过将输出重定向到io.StringIO
对象来捕获字符串中的输出。替换流从__enter__
方法返回,因此可用作with
语句的目标。with redirect_stdout(io.StringIO()) as f: help(pow) s = f.getvalue()
要将
help()
的输出发送到磁盘上的文件,请将输出重定向到常规文件:with open('help.txt', 'w') as f: with redirect_stdout(f): help(pow)
要将
help()
的输出发送到 sys.stderr:with redirect_stdout(sys.stderr): help(pow)
请注意,对
sys.stdout
的全局副作用意味着此上下文管理器不适合在库代码和大多数线程应用程序中使用。它也不会影响子进程的输出。但是,对于许多实用程序脚本来说,它仍然是一种有用的方法。此上下文管理器是可重入的。
3.4 版本新增。
- contextlib.redirect_stderr(new_target)¶
类似于
redirect_stdout()
,但将sys.stderr
重定向到另一个文件或类文件对象。此上下文管理器是可重入的。
3.5 版本新增。
- contextlib.chdir(path)¶
用于更改当前工作目录的非并行安全上下文管理器。由于这会更改全局状态(即工作目录),因此不适合在大多数线程或异步上下文中使用。它也不适合大多数非线性代码执行,例如生成器(其中程序执行被暂时放弃)——除非明确需要,否则当此上下文管理器处于活动状态时,你不应该 yield。
这是对
chdir()
的一个简单包装,它会在进入时更改当前工作目录,并在退出时恢复旧的工作目录。此上下文管理器是可重入的。
3.11 版本新增。
- 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.AsyncContextDecorator¶
类似于
ContextDecorator
,但仅适用于异步函数。AsyncContextDecorator
的示例from asyncio import run from contextlib import AsyncContextDecorator class mycontext(AsyncContextDecorator): async def __aenter__(self): print('Starting') return self async def __aexit__(self, *exc): print('Finishing') return False
然后,该类可以像这样使用:
>>> @mycontext() ... async def function(): ... print('The bit in the middle') ... >>> run(function()) Starting The bit in the middle Finishing >>> async def function(): ... async with mycontext(): ... print('The bit in the middle') ... >>> run(function()) Starting The bit in the middle Finishing
3.10 版本新增。
- class contextlib.ExitStack¶
一个上下文管理器,旨在轻松地以编程方式组合其他上下文管理器和清理函数,特别是那些可选的或由输入数据驱动的上下文管理器和清理函数。
例如,可以按照以下方式在一个 with 语句中轻松处理一组文件
with ExitStack() as stack: files = [stack.enter_context(open(fname)) for fname in filenames] # All opened files will automatically be closed at the end of # the with statement, even if attempts to open files later # in the list raise an exception
__enter__()
方法返回ExitStack
实例,并且不执行其他操作。每个实例都维护一个已注册的回调堆栈,当实例关闭时(显式关闭或在
with
语句末尾隐式关闭)会以相反的顺序调用这些回调。请注意,当上下文堆栈实例被垃圾回收时,不会隐式调用回调。使用此堆栈模型是为了能够正确处理在其
__init__
方法中获取其资源的上下文管理器(例如文件对象)。由于注册的回调以注册的相反顺序调用,因此最终的行为就像使用了多个嵌套的
with
语句,其中包含已注册的回调集。这甚至扩展到异常处理 - 如果内部回调抑制或替换了异常,则外部回调将根据更新后的状态传递参数。这是一个相对底层的 API,负责正确展开退出回调堆栈的细节。它为以特定于应用程序的方式操作退出堆栈的更高级别的上下文管理器提供了合适的基础。
3.3 版本新增。
- enter_context(cm)¶
进入新的上下文管理器,并将其
__exit__()
方法添加到回调堆栈。返回值是上下文管理器自己的__enter__()
方法的结果。这些上下文管理器可以像直接作为
with
语句的一部分使用时一样抑制异常。在 3.11 版本中更改: 如果 cm 不是上下文管理器,则引发
TypeError
而不是AttributeError
。
- push(exit)¶
将上下文管理器的
__exit__()
方法添加到回调堆栈。由于 不 调用
__enter__
,因此可以使用此方法用上下文管理器自己的__exit__()
方法来覆盖__enter__()
实现的一部分。如果传递的对象不是上下文管理器,则此方法假定它是具有与上下文管理器的
__exit__()
方法相同签名的回调,并将其直接添加到回调堆栈。通过返回 true 值,这些回调可以像上下文管理器
__exit__()
方法一样抑制异常。传递的对象从函数返回,允许将此方法用作函数装饰器。
- callback(callback, /, *args, **kwds)¶
接受任意回调函数和参数,并将其添加到回调堆栈。
与其他方法不同,以这种方式添加的回调无法抑制异常(因为它们永远不会传递异常详细信息)。
传递的回调从函数返回,允许将此方法用作函数装饰器。
- pop_all()¶
将回调堆栈传输到新的
ExitStack
实例并返回它。此操作不会调用任何回调 - 相反,它们现在将在新的堆栈关闭时调用(显式关闭或在with
语句末尾隐式关闭)。例如,可以按照以下方式以“全有或全无”的操作打开一组文件
with ExitStack() as stack: files = [stack.enter_context(open(fname)) for fname in filenames] # Hold onto the close method, but don't call it yet. close_files = stack.pop_all().close # If opening any file fails, all previously opened files will be # closed automatically. If all files are opened successfully, # they will remain open even after the with statement ends. # close_files() can then be invoked explicitly to close them all.
- close()¶
立即展开回调堆栈,以注册的相反顺序调用回调。对于注册的任何上下文管理器和退出回调,传递的参数将指示没有发生异常。
- class contextlib.AsyncExitStack¶
一个 异步上下文管理器,类似于
ExitStack
,支持组合同步和异步上下文管理器,以及具有用于清理逻辑的协程。- coroutine enter_async_context(cm)¶
类似于
ExitStack.enter_context()
,但期望一个异步上下文管理器。在 3.11 版本中更改: 如果 cm 不是异步上下文管理器,则引发
TypeError
而不是AttributeError
。
- push_async_exit(exit)¶
类似于
ExitStack.push()
,但期望一个异步上下文管理器或一个协程函数。
- push_async_callback(callback, /, *args, **kwds)¶
类似于
ExitStack.callback()
,但期望一个协程函数。
- coroutine aclose()¶
类似于
ExitStack.close()
,但能正确处理 awaitables。
继续
asynccontextmanager()
的示例async with AsyncExitStack() as stack: connections = [await stack.enter_async_context(get_connection()) for i in range(5)] # All opened connections will automatically be released at the end of # the async with statement, even if attempts to open a connection # later in the list raise an exception.
3.7 版本新增。
示例和方法¶
本节介绍一些示例和方法,以便有效地利用 contextlib
提供的工具。
支持可变数量的上下文管理器¶
ExitStack
的主要用例是在类文档中给出的:在单个 with
语句中支持可变数量的上下文管理器和其他清理操作。这种可变性可能来自于所需上下文管理器的数量由用户输入驱动(例如打开用户指定的文件集合),或者来自于某些上下文管理器是可选的。
with ExitStack() as stack:
for resource in resources:
stack.enter_context(resource)
if need_special_resource():
special = acquire_special_resource()
stack.callback(release_special_resource, special)
# Perform operations that use the acquired resources
捕获 __enter__
方法中的异常¶
有时,需要捕获 __enter__
方法实现中的异常,而不会 无意中捕获来自 with
语句主体或上下文管理器的 __exit__
方法的异常。通过使用 ExitStack
,可以稍微分离上下文管理协议中的步骤,以便允许这样做
stack = ExitStack()
try:
x = stack.enter_context(cm)
except Exception:
# handle __enter__ exception
else:
with stack:
# Handle normal case
实际上需要这样做可能表明底层 API 应该提供一个直接的资源管理接口,以便与 try
/except
/finally
语句一起使用,但并非所有的 API 在这方面都设计良好。当上下文管理器是唯一提供的资源管理 API 时,ExitStack
可以更轻松地处理无法在 with
语句中直接处理的各种情况。
在 __enter__
实现中进行清理¶
正如 ExitStack.push()
的文档中所述,如果 __enter__()
实现中的后续步骤失败,则此方法可用于清理已分配的资源。
这是一个示例,说明如何为接受资源获取和释放函数以及可选的验证函数的上下文管理器执行此操作,并将其映射到上下文管理协议
from contextlib import contextmanager, AbstractContextManager, ExitStack
class ResourceManager(AbstractContextManager):
def __init__(self, acquire_resource, release_resource, check_resource_ok=None):
self.acquire_resource = acquire_resource
self.release_resource = release_resource
if check_resource_ok is None:
def check_resource_ok(resource):
return True
self.check_resource_ok = check_resource_ok
@contextmanager
def _cleanup_on_error(self):
with ExitStack() as stack:
stack.push(self)
yield
# The validation check passed and didn't raise an exception
# Accordingly, we want to keep the resource, and pass it
# back to our caller
stack.pop_all()
def __enter__(self):
resource = self.acquire_resource()
with self._cleanup_on_error():
if not self.check_resource_ok(resource):
msg = "Failed validation for {!r}"
raise RuntimeError(msg.format(resource))
return resource
def __exit__(self, *exc_details):
# We don't need to duplicate any of our resource release logic
self.release_resource()
替换任何使用 try-finally
和标志变量的情况¶
您有时会看到的一种模式是带有标志变量的 try-finally
语句,用于指示是否应执行 finally
子句的主体。在其最简单的形式(不能仅仅通过使用 except
子句来处理)中,它看起来像这样
cleanup_needed = True
try:
result = perform_operation()
if result:
cleanup_needed = False
finally:
if cleanup_needed:
cleanup_resources()
与任何基于 try
语句的代码一样,这可能会给开发和审查带来问题,因为设置代码和清理代码最终可能会被任意长的代码段分隔开。
ExitStack
可以注册一个回调函数,以便在 with
语句结束时执行,然后稍后决定是否跳过执行该回调函数
from contextlib import ExitStack
with ExitStack() as stack:
stack.callback(cleanup_resources)
result = perform_operation()
if result:
stack.pop_all()
这允许预先明确预期的清理行为,而不是需要单独的标志变量。
如果特定应用程序大量使用这种模式,则可以通过一个小的辅助类进一步简化它
from contextlib import ExitStack
class Callback(ExitStack):
def __init__(self, callback, /, *args, **kwds):
super().__init__()
self.callback(callback, *args, **kwds)
def cancel(self):
self.pop_all()
with Callback(cleanup_resources) as cb:
result = perform_operation()
if result:
cb.cancel()
如果资源清理尚未整齐地捆绑到一个独立的函数中,则仍然可以使用 ExitStack.callback()
的装饰器形式来预先声明资源清理
from contextlib import ExitStack
with ExitStack() as stack:
@stack.callback
def cleanup_resources():
...
result = perform_operation()
if result:
stack.pop_all()
由于装饰器协议的工作方式,以这种方式声明的回调函数不能接受任何参数。相反,任何要释放的资源都必须作为闭包变量访问。
使用上下文管理器作为函数装饰器¶
ContextDecorator
使得既可以在普通的 with
语句中使用上下文管理器,也可以作为函数装饰器使用。
例如,有时用一个记录器来包装函数或语句组很有用,该记录器可以跟踪进入时间和退出时间。与其为该任务编写函数装饰器和上下文管理器,不如从 ContextDecorator
继承,在一个定义中提供两种功能
from contextlib import ContextDecorator
import logging
logging.basicConfig(level=logging.INFO)
class track_entry_and_exit(ContextDecorator):
def __init__(self, name):
self.name = name
def __enter__(self):
logging.info('Entering: %s', self.name)
def __exit__(self, exc_type, exc, exc_tb):
logging.info('Exiting: %s', self.name)
此类的实例可以用作上下文管理器
with track_entry_and_exit('widget loader'):
print('Some time consuming activity goes here')
load_widget()
也可以作为函数装饰器
@track_entry_and_exit('widget loader')
def activity():
print('Some time consuming activity goes here')
load_widget()
请注意,当将上下文管理器用作函数装饰器时,还有一个额外的限制:无法访问 __enter__()
的返回值。如果需要该值,则仍然必须使用显式的 with
语句。
单次使用、可重用和可重入的上下文管理器¶
大多数上下文管理器的编写方式意味着它们只能在 with
语句中使用一次。这些单次使用的上下文管理器每次使用时都必须重新创建 - 尝试第二次使用它们会触发异常或以其他方式无法正常工作。
这个常见的限制意味着通常建议在 with
语句的头部直接创建上下文管理器,在这些语句中使用它们(如上面的所有用法示例所示)。
文件是有效的单次使用上下文管理器的示例,因为第一个 with
语句将关闭该文件,阻止使用该文件对象进行任何进一步的 IO 操作。
使用 contextmanager()
创建的上下文管理器也是单次使用的上下文管理器,如果尝试第二次使用它们,将会报错,因为底层的生成器未能 yield。
>>> 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()
和 chdir()
也是如此。 以下是一个非常简单的可重入使用示例
>>> 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
绑定到不同的流来对系统状态进行全局修改。
可复用上下文管理器¶
与单次使用和可重入上下文管理器不同的是“可复用”上下文管理器(或者,更明确地说,“可复用,但不可重入”的上下文管理器,因为可重入的上下文管理器也是可复用的)。这些上下文管理器支持多次使用,但是如果特定的上下文管理器实例已经在包含的 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