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 语句中使用,但有时需要管理的资源本身不是上下文管理器,也没有实现可用于 contextlib.closingclose() 方法。

一个抽象的例子如下,以确保正确的资源管理:

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

被装饰的函数在被调用时必须返回一个 生成器-迭代器。此迭代器必须只 yield 一个值,该值将被绑定到 with 语句的 as 子句中的目标(如果有的话)。

在生成器 yield 的地方,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() 也会被调用。

备注

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

contextlib.aclosing(thing)

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

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__ 也保留其可选的异常处理功能。

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 作为混入类(mixin class)来扩展:

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

通过返回真值,这些回调可以像上下文管理器的 __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,支持组合同步和异步上下文管理器,以及用于清理逻辑的协程。

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

async 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(),但期望一个协程函数。

async aclose()

类似于 ExitStack.close(),但能正确处理可等待对象。

继续 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() 创建的上下文管理器也是一次性上下文管理器,如果试图第二次使用它们,它们会抱怨底层生成器未能 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