29.6. contextlib - with的实用程序 - 状态上下文

源代码: Lib / contextlib.py

此模块为涉及with语句的常见任务提供实用程序。有关详细信息,请参阅Context Manager TypesWith Statement Context Managers

29.6.1. Utilities

提供的函数和类:

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

一个简单的例子(这不推荐作为一个真正的方式生成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 --iterator。这个迭代器必须产生一个值,它将绑定到with语句的as子句的目标(如果有)。

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

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

在版本3.2中更改:使用ContextDecorator

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

此上下文管理器为reentrant

版本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的全局副作用意味着此上下文管理器不适合在库代码和大多数线程应用程序中使用。它也对子进程的输出没有影响。然而,它对许多实用程序脚本仍然是一个有用的方法。

此上下文管理器为reentrant

版本3.4中的新功能。

contextlib.redirect_stderr(new_target)

redirect_stdout()类似,但将sys.stderr重定向到另一个文件或类文件对象。

此上下文管理器为reentrant

版本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 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语句结束时显式或隐式),以相反的顺序调用。注意,当上下文栈实例被垃圾收集时,回调是隐式调用。

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

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

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

版本3.3中的新功能。

enter_context(cm)

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

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

push(exit)

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

由于__enter__被调用,此方法可用于覆盖__enter__()实现的一部分,具有上下文管理器自己的__exit__()方法。

如果传递的不是上下文管理器的对象,此方法假定它是与上下文管理器的__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. Examples and Recipes

本节描述了有效使用contextlib提供的工具的一些示例和配方。

29.6.2.1. Supporting a variable number of context managers

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. Simplifying support for single optional context managers

在单个可选上下文管理器的特定情况下,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. Catching exceptions from __enter__ methods

有时候希望从__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语句中直接处理的各种情况。

29.6.2.4. Cleaning up in an __enter__ implementation

ExitStack.push()文档中所述,如果__enter__()实现中的后续步骤失败,此方法可用于清除已分配的资源。

这里有一个例子,为接受资源获取和释放功能的上下文管理器,以及一个可选的验证功能,并将它们映射到上下文管理协议:

from contextlib import contextmanager, ExitStack

class ResourceManager:

    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. Replacing any use of try-finally and flag variables

有时您会看到的模式是一个带有一个标志变量的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. Using a context manager as a function decorator

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: {}'.format(self.name))

    def __exit__(self, exc_type, exc, exc_tb):
        logging.info('Exiting: {}'.format(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. Single use, reusable and reentrant context managers

大多数上下文管理器的写入方式意味着它们只能在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. Reentrant context managers

更复杂的上下文管理器可以是“可重入的”。这些上下文管理器不仅可以用于多个with语句中,也可以在里使用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

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

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

29.6.3.2. Reusable context managers

与单用和可重入上下文管理器不同的是“可重用”上下文管理器(或者,完全显式,“可重用,但不可重入”上下文管理器,因为可重用的上下文管理器也是可重用的)。这些上下文管理器支持多次使用,但是如果特定上下文管理器实例已经在contains 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