上下文管理器(Context managers)允许你在有需要的时候,精确地分配和释放资源。
with
语法
使用上下文管理器最广泛的案例就是with
语句了。 想象下你有两个需要结对执行的相关操作,然后还要在它们中间放置一段代码。 上下文管理器就是专门让你做这种事情的。 with
的语法格式:
with context_expression [as target(s)]:
with-body
举个使用了with
的例子:
with open('some_file', 'w') as opened_file:
opened_file.write('Hola!')
上面这段代码打开了一个文件,往里面写入了一些数据,然后关闭该文件。如果在往文件写数据时发生异常,它也会尝试去关闭文件。上面那段代码与这一段是等价的:
file = open('some_file', 'w')
try:
file.write('Hola!')
finally:
file.close()
当与第一个例子对比时,我们可以看到,通过使用with
,许多样板代码被消掉了。 这就是with
语句的主要优势,它确保我们的文件会被关闭,而不用关注嵌套代码如何退出。
上下文管理器的常见用例,是资源的加锁和解锁,以及关闭已打开的文件。
基于类的实现
with
的语法非常简单,只需要 with
一个表达式,就可以执行自定义的业务逻辑。但是with
后面的表达式并不是可以任意写的。要想使用 with
语法块,with
后面的的对象需要实现上下文管理器协议。
一个类在 Python 中,只要实现以下方法,就实现了上下文管理器协议:
__enter__
:在进入with
语法块之前调用,返回值会赋值给with
的 target;__exit__
:在退出with
语法块时调用,一般用作异常处理。
也就是说,一个上下文管理器的类,最起码要定义 __enter__
和 __exit__
方法。 接下来,我们来看一个构造自己的上下文管理器的例子:
class File(object):
def __init__(self, file_name, method):
self.file_obj = open(file_name, method)
def __enter__(self):
return self.file_obj
def __exit__(self, type, value, traceback):
self.file_obj.close()
通过定义__enter__
和 __exit__
方法,我们就可以在with
语句里使用它。
with File('demo.txt', 'w') as opened_file:
opened_file.write('Hola!')
首先,要知道的是__exit__
函数接受三个参数。这些参数对于每个上下文管理器类中的 __exit__
方法都是必须的。我们来谈谈在底层都发生了什么。
with
语句先暂存了File
类的__exit__
方法。- 然后它调用
File
类的__enter__
方法。 __enter__
方法打开文件并返回给with
语句。- 打开的文件句柄被传递给
opened_file
参数。 - 我们使用
.write()
来写文件。 with
语句调用之前暂存的__exit__
方法。__exit__
方法关闭了文件。
处理异常
接着,我们来谈谈__exit__
方法的这三个参数:type
,value
和 traceback
。 在第4步和第6步之间,如果发生异常,Python 会将异常的type
,value
和 traceback
传递给 __exit__
方法。 它让 __exit__
方法来决定如何关闭文件以及是否需要其他步骤。
也就是说如果在 with
语句块内发生了异常,那么 __exit__
方法可以拿到关于异常的详细信息:
type
:异常类型value
:异常对象tb
:异常堆栈信息
当异常发生时,with
语句会采取如下步骤。
- 它把异常的
type
,value
和traceback
传递给__exit__
方法。 - 它让
__exit__
方法来处理异常。 - 如果
__exit__
返回的是True
,那么这个异常就被优雅地处理了。 - 如果
__exit__
返回的是True
以外的任何东西,那么这个异常将被with
语句抛出。
基于生成器的实现
对于需要上下文管理的场景,除了自己实现 __enter__
和 __exit__
之外,还可以更简单。我们可以使用 Python 标准库提供的contextlib
模块,来简化我们的代码。
使用contextlib
模块,我们可以把上下文管理器当成一个装饰器来使用。其中,contextlib
模块提供了 contextmanager
装饰器和closing
方法。
来看一个简单的例子:
from contextlib import contextmanager
@contextmanager
def test():
print('before')
yield 'hello'
print('after')
with test() as t:
print(t)
在这个例子中,使用 contextmanager
装饰器和yield
配合,实现了和前文上下文管理器相同的功能,它的执行流程如下:
- 执行
test()
方法,先打印出before
- 执行
yield 'hello'
,test
方法返回,hello
返回值会赋值给with
语句块的t
变量 - 执行
with
语句块内的逻辑,打印出t
的值hello
- 又回到
test
方法中,执行yield
后面的逻辑,打印出after
这样一来,当我们使用这个 contextmanager
装饰器后,就不用再写一个类来实现上下文管理协议,只需要用一个方法装饰对应的方法,就可以实现相同的功能。
不过有一点需要我们注意:在使用 contextmanager
装饰器时,如果被装饰的方法内发生了异常,那么我们需要在自己的方法中进行异常处理,否则将不会执行 yield
之后的逻辑。
我们再来看 contextlib
提供的 closing
方法如何使用。closing
主要用在已经实现 close
方法的资源对象上:
from contextlib import closing
class Test():
def close(self): # 定义了 close 方法才可以使用 closing 装饰器
print('closed')
# with 执行结束后 自动执行 close 方法
with closing(Test()):
print('do something')
从执行结果我们可以看到,with
语句块执行结束后,会自动调用 Test
实例的 close
方法。所以,对于需要自定义关闭资源的场景,我们可以使用这个方法配合 with
来完成。
总结
with
非常适合用需要对于上下文处理的场景,例如操作文件、Socket,这些场景都需要在执行完业务逻辑后,释放资源。