2.24. 上下文管理器

with 语句会先运行设置代码,然后运行主体,再运行清理代码——并保证即使主体在中途失败,清理代码也会运行。提供设置和清理的那一对方法称为上下文管理器

其形式为:

with <expression> as <name>:
    <body>

该表达式返回一个上下文管理器。Python 调用它的 __enter__ 方法,可选地将结果绑定到 <name>,运行主体,然后调用 __exit__ ——无论主体是正常完成还是抛出了异常。

流程图:先运行 __enter__,然后是主体; 主体要么正常完成,要么抛出异常;无论哪种情况, 最后都会运行 __exit__。

无论代码块内部发生什么,__exit__ 都会在块的末尾运行。

2.24.1. 使用上下文管理器

最典型的例子是打开文件:

with open("data.txt") as f:
    text = f.read()

# f is now closed, even if read() failed

open() 返回一个文件对象,它本身就是一个上下文管理器;__enter__ 返回该文件,__exit__ 关闭它。with 块让"用完后总是关闭文件"成为默认行为,而不是某件调用者必须记住的事。

2.24.1.1. 多个上下文管理器

单条 with 语句可以一次进入多个上下文管理器,以逗号分隔:

with open("input.txt") as src, open("output.txt", "w") as dst:
    dst.write(src.read())

这等价于两个嵌套的 with 块,但更扁平。这些管理器按从左到右的顺序进入,并以相反顺序退出;如果右侧管理器的 __enter__ 抛出异常,左侧管理器的 __exit__ 仍会运行。

2.24.2. 编写上下文管理器

任何带有 __enter____exit__ 方法的类都可以作为上下文管理器:

class Section:
    def __init__(self, label):
        self.label = label

    def __enter__(self):
        print("---", self.label, "---")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("--- end", self.label, "---")
        return False

with Section("setup"):
    print("doing the work")

输出::

--- setup ---
doing the work
--- end setup ---

__exit__ 接收三个描述结束该块的异常的参数,如果块正常完成则接收三个 None 值。返回 False(或 None)会让任何异常在清理之后继续传播;返回 True 则会吞掉该异常。

对于任何具有"打开/关闭"或"获取/释放"生命周期的资源,都应使用上下文管理器——而不仅仅是文件。这种模式让清理与设置在二者被引入的同一处成对出现,因此在一个长函数中间忘记某个关闭操作也不会泄漏资源。