contextlibwith 语句上下文的实用工具

源代码: Lib/contextlib.py


此模块为涉及 with 语句的常见任务提供了实用工具。有关更多信息,另请参阅 上下文管理器类型with 语句上下文管理器

实用工具

提供的函数和类

class contextlib.AbstractContextManager

一个 抽象基类,用于实现 object.__enter__()object.__exit__() 的类。为 object.__enter__() 提供了一个默认实现,它返回 self,而 object.__exit__() 是一个抽象方法,默认返回 None。另请参阅 上下文管理器类型 的定义。

在 3.6 版本中添加。

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 语句中的块被执行。在块退出后,生成器恢复。如果块中发生未处理的异常,它将在生成器中生成时重新引发。因此,你可以使用 tryexceptfinally 语句来捕获错误(如果有),或确保进行一些清理。如果捕获异常只是为了记录或执行某些操作(而不是完全禁止),生成器必须重新引发该异常。否则,生成器上下文管理器会向 with 语句指示该异常已处理,并且执行将从 with 语句紧随其后的语句恢复。

contextmanager() 使用 ContextDecorator,因此它创建的上下文管理器可以用作装饰器,也可以用在 with 语句中。当用作装饰器时,在每次函数调用时都会隐式创建一个新的生成器实例(这允许 contextmanager() 创建的“一次性”上下文管理器满足上下文管理器支持多次调用以用作装饰器的要求)。

在版本 3.2 中更改:使用 ContextDecorator

@contextlib.asynccontextmanager

类似于 contextmanager(),但创建一个 异步上下文管理器

此函数是一个 装饰器,可用于为 async with 语句异步上下文管理器定义工厂函数,无需创建类或单独的 __aenter__()__aexit__() 方法。必须将其应用于 异步生成器 函数。

一个简单的示例

from contextlib import asynccontextmanager

@asynccontextmanager
async def get_connection():
    conn = await acquire_db_connection()
    try:
        yield conn
    finally:
        await release_db_connection(conn)

async def get_all_users():
    async with get_connection() as conn:
        return conn.query('SELECT ...')

在版本 3.7 中添加。

使用 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://www.pythonlang.cn')) as page:
    for line in page:
        print(line)

无需显式关闭 page。即使发生错误,当 with 块退出时,也会调用 page.close()

注意

管理资源的大多数类型都支持 上下文管理器 协议,该协议在离开 with 语句时关闭 thing。因此,closing() 最适用于不支持上下文管理器的第三方类型。此示例纯属说明目的,因为 urlopen() 通常用在上下文管理器中。

contextlib.aclosing(thing)

返回一个异步上下文管理器,它在块完成时调用thingaclose()方法。这基本上等同于

from contextlib import asynccontextmanager

@asynccontextmanager
async def aclosing(thing):
    try:
        yield thing
    finally:
        await thing.aclose()

值得注意的是,aclosing()支持确定性地清理异步生成器,当它们通过break或异常提前退出时。

from contextlib import aclosing

async with aclosing(my_generator()) as values:
    async for value in values:
        if value == 42:
            break

例如

此模式确保生成器的异步退出代码与其迭代在同一上下文中执行(以便异常和上下文变量按预期工作,并且退出代码不会在其依赖的某些任务的生命周期之后运行)。

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)

非并行安全上下文管理器,用于更改当前工作目录。由于这会更改全局状态(工作目录),因此不适合在大多数线程或异步上下文中使用。它也不适合大多数非线性代码执行,如生成器,其中程序执行暂时放弃 - 除非明确需要,否则在该上下文管理器处于活动状态时不应产生。

这是一个 chdir() 的简单包装器,它在进入时更改当前工作目录,并在退出时还原旧目录。

此上下文管理器是可重入的。

在版本 3.11 中添加。

class contextlib.ContextDecorator

一个基类,使上下文管理器也可以用作装饰器。

ContextDecorator 继承的上下文管理器必须像往常一样实现 __enter____exit__。即使用作装饰器,__exit__ 也保留其可选的异常处理。

ContextDecoratorcontextmanager() 使用,因此您可以自动获得此功能。

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 作为混合类进行扩展

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

此模式确保生成器的异步退出代码与其迭代在同一上下文中执行(以便异常和上下文变量按预期工作,并且退出代码不会在其依赖的某些任务的生命周期之后运行)。

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__() 方法具有相同签名的回调,并直接将其添加到回调堆栈。

通过返回真值,这些回调可以像上下文管理器 __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()

立即展开回调堆栈,按注册的相反顺序调用回调。对于任何注册的上下文管理器和退出回调,传入的参数将指示未发生异常。

contextlib.AsyncExitStack

一个异步上下文管理器,类似于ExitStack,它支持组合同步和异步上下文管理器,以及具有用于清理逻辑的协程。

close()方法未实现;aclose()必须用作替代。

协程 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(),但需要一个协程函数。

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

如所示, 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__ 实现中进行清理

正如 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 语句。

另请参见

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