在使用Python编程中,可以会经常碰到这种情况:有一个特殊的语句块,在执行这个语句块之前需要先执行一些准备动作;当语句块执行完成后,需要继续执行一些收尾动作。
例如:当需要操作文件或数据库的时候,首先需要获取文件句柄或者数据库连接对象,当执行完相应的操作后,需要执行释放文件句柄或者关闭数据库连接的动作。
又如,当多线程程序需要访问临界资源的时候,线程首先需要获取互斥锁,当执行完成并准备退出临界区的时候,需要释放互斥锁。
对于这些情况,Python中提供了上下文管理器(Context Manager)的概念,可以通过上下文管理器来定义/控制代码块执行前的准备动作,以及执行后的收尾动作。
那么在Python中怎么实现一个上下文管理器呢?这里,又要提到两个"魔术方法",__enter__和__exit__,下面就是关于这两个方法的具体介绍。
也就是说,当我们需要创建一个上下文管理器类型的时候,就需要实现__enter__和__exit__方法,这对方法就称为上下文管理协议(Context Manager Protocol),定义了一种运行时上下文环境。
在Python中,可以通过with语句来方便的使用上下文管理器,with语句可以在代码块运行前进入一个运行时上下文(执行__enter__方法),并在代码块结束后退出该上下文(执行__exit__方法)。
with语句的语法如下:
在Python的内置类型中,很多类型都是支持上下文管理协议的,例如file,thread.LockType,threading.Lock等等。这里我们就以file类型为例,看看with语句的使用。
当需要写一个文件的时候,一般都会通过下面的方式。代码中使用了try-finally语句块,即使出现异常,也能保证关闭文件句柄。
其实,Python的内置file类型是支持上下文管理协议的,可以直接通过内建函数dir()来查看file支持的方法和属性:
所以,可以通过with语句来简化上面的代码,代码的效果是一样的,但是使用with语句的代码更加的简洁:
对于自定义的类型,可以通过实现__enter__和__exit__方法来实现上下文管理器。
看下面的代码,代码中定义了一个MyTimer类型,这个上下文管理器可以实现代码块的计时功能:
下面结合with语句使用这个上下文管理器:
代码输出结果为:

在使用上下文管理器中,如果代码块 (with_suite)产生了异常,__exit__方法将被调用,而__exit__方法又会有不同的异常处理方式。
当__exit__方法退出当前运行时上下文时,会并返回一个布尔值,该布尔值表明了"如果代码块 (with_suite)执行中产生了异常,该异常是否须要被忽略"。
1. __exit__返回False,重新抛出(re-raised)异常到上层
修改前面的例子,在MyTimer类型中加入了一个参数"ignoreException"来表示上下文管理器是否会忽略代码块 (with_suite)中产生的异常。
运行这段代码,会得到以下结果,由于__exit__方法返回False,所以代码块 (with_suite)中的异常会被继续抛到上层代码。

2. __exit__返回Ture,代码块 (with_suite)中的异常被忽略
将代码改为__exit__返回为True的情况:
运行结果就变成下面的情况,代码块 (with_suite)中的异常被忽略了,代码继续运行:

一定要小心使用__exit__返回Ture的情况,除非很清楚为什么这么做。
3. 通过__exit__函数完整的签名获取更多异常信息
对于__exit__函数,它的完整签名如下,也就是说通过这个函数可以获得更多异常相关的信息。
继续修改上面例子中的__exit__函数如下:
这次运行结果中,就显示出了更多异常相关的信息了:

本文介绍了Python中的上下文管理器,以及如何结合with语句来使用上下文管理器。
总结一下with 语句的执行流程:
在很多情况下,with语句可以简化代码,并增加代码的健壮性。