Skip to main content

29.6. contextlibwith -statement上下文的实用程序

源代码: Lib/contextlib.py


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

29.6.1. 实用程序

提供的函数和类:

class contextlib.AbstractContextManager

实现 object.__enter__()object.__exit__() 的类的 abstract base class。提供 object.__enter__() 的默认实现,其返回 self,而 object.__exit__() 是抽象方法,其默认返回 None。参见 上下文管理器类型 的定义。

3.6 新版功能.

@contextlib.contextmanager

此函数是一个 decorator,可用于为 with 语句上下文管理器定义工厂函数,而无需创建类或单独的 __enter__()__exit__() 方法。

一个简单的例子(这不推荐作为一个真正的生成HTML的方式!):

from contextlib import contextmanager

@contextmanager
def tag(name):
    print("<%s>" % name)
    yield
    print("</%s>" % name)

>>> with tag("h1"):
...    print("foo")
...
<h1>
foo
</h1>

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

在生成器产生的点,嵌套在 with 语句中的块被执行。然后在退出块之后恢复发电机。如果在块中发生未处理的异常,则在发生产生的点处在生成器内重新处理。因此,您可以使用 try ... except ... finally 语句来捕获错误(如果有),或确保执行某些清除操作。如果一个异常被捕获只是为了记录它或执行一些操作(而不是完全抑制它),生成器必须重新处理该异常。否则,生成器上下文管理器将向 with 语句指示已经处理了异常,并且执行将以紧跟在 with 语句之后的语句重新开始。

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

在 3.2 版更改: 使用 ContextDecorator

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

而不需要明确关闭 page。即使发生错误,在退出 with 块时将调用 page.close()

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

这个上下文管理器是 可重入

3.4 新版功能.

contextlib.redirect_stdout(new_target)

上下文管理器用于临时将 sys.stdout 重定向到另一个文件或类文件对象。

此工具为现有的函数或类输出硬连接到stdout的类增加了灵活性。

例如,help() 的输出通常被发送到 sys.stdout。您可以通过将输出重定向到 io.StringIO 对象来捕获字符串中的输出:

f = io.StringIO()
with redirect_stdout(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 新版功能.

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类来扩展:

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

每个实例维护一堆注册的回调,当实例关闭时(在 with 语句结束时显式地或隐式地),以相反的顺序调用它们。注意,当上下文栈实例被垃圾收集时,回调是 not 隐式调用。

使用此堆栈模型,以便可以正确处理在其 __init__ 方法(如文件对象)中获取其资源的上下文管理器。

因为注册的回调以注册的相反顺序被调用,所以最终表现得好像多个嵌套的 with 语句已经与注册的回调集合一起使用。这甚至扩展到异常处理 - 如果内部回调抑制或替换异常,则外部回调将基于更新的状态传递参数。

这是一个相对较低级别的API,它处理正确展开退出回调栈的细节。它为更高级别的上下文管理器提供了一个合适的基础,它以特定应用程序的方式处理退出栈。

3.3 新版功能.

enter_context(cm)

输入一个新的上下文管理器并将其 __exit__() 方法添加到回调栈。返回值是上下文管理器自己的 __enter__() 方法的结果。

这些上下文管理器可以如通常直接作为 with 语句的一部分使用来抑制异常。

push(exit)

向回调栈添加上下文管理器的 __exit__() 方法。

由于 __enter__not 调用的,所以该方法可以用于使用上下文管理器自己的 __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()

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

29.6.2. 示例和食谱

本节介绍一些有效使用 contextlib 提供的工具的示例和方法。

29.6.2.1. 支持可变数量的上下文管理器

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 语句来管理不原生地支持上下文管理协议的任意资源。

29.6.2.2. 简化对单个可选上下文管理器的支持

在单个可选上下文管理器的特定情况下,ExitStack 实例可以用作“无用”上下文管理器,允许容易地省略上下文管理器而不影响源代码的整体结构:

def debug_trace(details):
    if __debug__:
        return TraceContext(details)
    # Don't do anything special with the context in release mode
    return ExitStack()

with debug_trace():
    # Suite is traced in debug mode, but runs normally otherwise

29.6.2.3. 从 __enter__ 方法捕获异常

有时需要从 __enter__ 方法实现捕获异常,without 无意中从 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 语句中直接处理的各种情况。

29.6.2.4. 在 __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()

29.6.2.5. 替换任何使用 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(Callback, self).__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()

由于装饰器协议的工作方式,以这种方式声明的回调函数不能接受任何参数。相反,任何要释放的资源都必须作为闭包变量访问。

29.6.2.6. 使用上下文管理器作为函数装饰器

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 语句的规范,背景和示例。

29.6.3. 单用,可重用和可重用的上下文管理器

大多数上下文管理器的写法都意味着它们只能在 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

29.6.3.1. 可重入上下文管理器

更复杂的上下文管理器可以是“可重入的”。这些上下文管理器不仅可以在多个 with 语句中使用,还可以使用已经在使用相同上下文管理器的 inside a with 语句。

threading.RLock 是可重入上下文管理器的示例,suppress()redirect_stdout() 也是如此。这里有一个非常简单的可重入使用的例子:

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

现实世界的重入的例子更可能涉及多个函数互相调用,因此比这个例子复杂得多。

还要注意,可重入是 not 与线程安全相同的事情。例如,redirect_stdout() 绝对不是线程安全的,因为它通过将 sys.stdout 绑定到不同的流来对系统状态进行全局修改。

29.6.3.2. 可重用的上下文管理器

与单用和可重入上下文管理器不同的是“可重用”上下文管理器(或者,完全显式,“可重用,但不可重入”上下文管理器,因为可重用的上下文管理器也是可重用的)。这些上下文管理器支持多次使用,但如果特定的上下文管理器实例已经在contains with语句中使用,那么这些上下文管理器将会失败(或者无法正常工作)。

threading.Lock 是可重用的,但不是可重入的上下文管理器的示例(对于可重入锁,需要改用 threading.RLock)。

可重用但不可重入的上下文管理器的另一个示例是 ExitStack,因为它在离开任何with语句时调用 all 当前注册的回调,而不管这些回调是在哪里添加:

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