自学Python:编程基础、科学计算及数据分析
上QQ阅读APP看书,第一时间看更新

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()方法最终被调用了,比如打开的网页: