3.4 上下文管理器与with语句
之前在介绍文件读写的时候我们说过,当一个文件打开的时候,可能会遇到不正常关闭的问题,这样可能会影响我们读写文件。
不仅是文件,当我们处理资源的时候都可能会遇到这样的问题,这些资源包括文件、线程、数据库、网络连接等等。
写入文件时,如果文件没有被正常关闭,会导致某些内容没有来得及写入的情况。
例如,用一个循环向文件中写入数据:
当i循环到500的时候抛出了一个ZeroDivisionError,程序中断,上一行应该执行了写入“line 500”的操作。
打开这个文件,我们可能在文件结尾会看到类似如下的情况:
对于上面的情况,可以采用try块的方式,在finally中确保文件f被正确关闭:
finally能够保证f.close()正常执行。
打开文件会发现,文件的结尾保存的是正常的结果:
资源使用的问题十分常见,都使用try块处理显得不够简洁。为此,Python提供了上下文管理器的机制来解决这个问题,它通常与关键字with一起使用。
对于上面的例子,我们用with语句调用的方式为:
这与使用try块的效果相同,但是简洁了许多。
3.4.1 上下文管理器的原理
1. 基本形式和with语句
with语句的基本用法如下:
其中<expression>是一个上下文管理器。
上下文管理器(Context Manager)是一个实现了.__enter__()方法和.__exit__()方法的对象。
文件对象包含这两个方法,所以是一个合法的上下文管理器,可以用在with语句中:
在with语句中,上下文管理器的.__enter__()方法会在<statements>执行前执行,而.__exit__()方法会在<statements>执行结束后执行。
任何实现了这两种方法的对象都是一个合法的上下文管理器。不过,上下文管理器的.__exit__()方法需要接受3个额外参数(加上self是四个)。
例如,我们定义这样一个上下文管理器:
使用这个管理器:
如果<statements>在执行过程中抛出了异常,.__exit__()方法会先被执行,然后抛出异常:
2. 方法.__enter__()的返回值
为了在<statements>中使用文件对象,我们使用了as关键字的形式,将open()函数返回的文件对象赋给了f。事实上,as关键字只是将上下文管理器.__enter__()方法的返回值赋给了f,而文件对象的.__enter__()方法的返回值刚好是它本身:
我们修改TestManager的定义,将.__enter__()的返回值修改为字符串"My value!":
然后用as关键字得到这个返回值:
返回这个上下文管理器本身是一种常用的设计:
3. 方法.__exit__()与异常处理
在定义上下文管理器时,方法.__exit__()需要接受额外的参数,这些额外参数与异常处理相关。
重新定义TestManager,将这些参数打印出来:
没有异常时:
当运行过程中抛出异常时:
当运行出现异常时,这三个参数包含的是异常的具体信息。
在上面的例子中,我们只是简单的显示了异常的值,并没有像try-except块中的except块部分一样对异常进行处理,所以异常在执行完.__exit__()方法后被继续抛出了。
如果不想让异常继续抛出,我们只需要将.__exit__()方法的返回值设为True:
3.4.2 模块contextlib
Python提供了contextlib模块来方便我们使用上下文管理器。
1.contextmanager装饰器
contextlib模块提供了装饰器contextmanager来实现一个简单的“上下文管理器”:
contextmanager作为一个装饰器,对所修饰的函数有固定的要求:
该函数必须是一个生成器,且yield只被执行一次。在该函数中,yield之前的部分可以看成是.__enter__()方法的部分,yield返回的值可以看成是.__enter__()方法返回的值,yield之后的部分可以看成是.__exit__()方法的部分。
函数test_manager()返回了一个上下文管理器:
使用yield的返回值:
不过,这样定义出来的“上下文管理器”并不能在出错的时候保证后续处理被执行:
使用contextmanager构造的上下文管理器时,如果抛出异常,那么这个异常会在yield的地方重新被抛出,我们可以使用try块的形式对yield的部分进行处理。此外,为了保证对应.__exit__()的部分始终被执行,我们需要将yield后面的部分放入finally块中:
2.closing函数
contextlib模块中常用的还有closing()函数。该函数接受一个对象,返回一个确保该对象的.close()方法被调用的上下文管理器,它相当于这样的一个函数:
使用这种结构,我们能确保<object>的.close()方法最终被调用了,比如打开的网页: