考虑一个常见的用例——打开文件,通过使用Python内置的open函数打开文件,打开一个文件后,关闭文件就是你的责任了,如下所示:

try:
    f = open('path/to/filename', 'r')
    content = f.read()
finally:
    f.close()

使用finally子句确保无论发生什么,文件都将被关闭。

with语句

如何使用上下文管理器完成同样的功能——打开文件并确保被正确关闭?上下文管理器在Python2.5中引入,并新增了对应的关键字:with,使用with语句可以进入上下文管理器。

而Python的内置函数open同样可以作为上下文管理器使用,以下的代码与上面的效果相同:

with open('path/to/filename', 'r') as f:
    content = f.read()

从本质上讲,实际上是with语句对其后的代码进行求值(也就是open函数),该表达式会返回一个对象,该对象包含两个特殊的方法:__enter__和__exit__,__enter__方法的返回值会赋值给as关键字后面的变量。

需要注意的是,在with后的表达式的返回结果没有被赋值给任何所谓的变量,而只是其对应的__enter__方法的返回值被赋值给as后的变量。

简单性是使用上下文管理器的重要原因,更重要的是,记住用于异常处理和清理的代码有时会非常复杂,并且在不同的地方应用也非常麻烦。与装饰器相同的是,使用上下文管理器的关键原因在于避免代码重复。

__enter__和__exit__方法

记住,with语句的表达式的作用仅仅是返回一个遵从特定协议的对象,该对象必须定义的有__enter__和__exit__方法,当该对象被返回时,其对应的__enter__方法立即执行,如果有as语句,__enter__的返回值会被赋给as后面的变量。

往往__enter__方法内进行一些预处理或者说一些配置,比如连接数据库、打开文件返回相应的连接对象或文件描述符,__enter__方法除了self参数外,不接受其他的任何参数。

__exit__方法往往在退出上下文管理器时被执行,除了self参数外,带有3个位置参数:异常类型、异常实例、回溯,这三个参数在没有异常发生时,都会被设为None,如果有异常时,则会被填充。

考虑下面一个简单的上下文管理器实现:

class ContextManager(object):
    def __init__(self) -> None:
        self.entered = False

    def __enter__(self):
        self.entered = True

    def __exit__(self, ex_type, ex_instance, traceback):
        self.entered = False

该上下文管理器只是在进入时把entered变量置为True,退出时置为False,下面我们创建一个ContextManager对象:

>>> cm = ContextManager()
>>> cm.entered
False

可以看到此时cm的entered的值为False,进一步我们使用这个上下文管理器对象cm:

>>> with cm:
...   cm.entered
...
True

当在被with语句包装的代码块里也即进入到上下文管理器,entered的值变成了True,这就是__enter__方法的作用,在进入时被调用。而当离开了with语句代码块的作用域,也即离开上下文管理器时,__exit__方法被执行,entered会再次被置为False:

>>> cm.entered
False