本文主要介绍Python 中的with语法,该语法从Python 2.5中作为__future__
模块加入,在Python 2.6中加入到正式环境。
对个人来说,主要有两点收获:
- 如何让自己的对象支持
with
语法 - 发现了contextlib库
正文
with
语法简化了之前使用try...finally
代码块来确保清理代码被执行的写法。本节会跟大家讨论下with
语法的常见用法。下一节会介绍它的实现细节以及如何在自己的对象中支持with
语法。
with
语法作为一个新的流程控制体,下面是它的基本结构:
|
|
对这个表达式求值后会得到一个支持上下文管理协议(即__enter__()
和__exit()__
)方法的对象。
对象的__enter__()
方法会在with-block被执行时调用,因此可以用来执行一些预处理代码。如果表达式中写了as vaiable
部分的话,这个方法同时会返回一个值,这个值会和变量名variable进行绑定。(注意: variable并不会绑定到expression表达式的结果上)。
with-block代码在执行结束后会调用对象的__exit__()
方法,即使抛出了异常也不会例外。这样清理代码总是会被执行到。
在 Python 2.5中你需要使用如下代码启用with
语法(之后的版本默认开启):
|
|
Python标准库中的一些对象现在支持使用with
语法来进行上下文管理。用文件对象举例说明:
|
|
当这段代码执行完后,即使for
循环过程中遇到了异常,文件对象f也会被自动关闭。
注意: 在这个例子中,f和
open()
方法创建的对象一样,因为file.__enter()__
返回的是自身self
threading
模块的锁和条件变量同样支持with
语法:
|
|
区块代码会在执行前自动获取锁,执行完毕后自动释放锁。
模块decimal
中新的localcontext()
函数简化了当前decimal
上下文的保存和复原,这样在with
的代码块中可以指定计算所需的精度以及舍入特性而不影响到外面的代码。
|
|
编写上下文管理
当前的with
语法相当完善了,大多数的开发人员仅仅是在已有的对象上使用with
语法,而不需要知道它们的实现细节,因此本节可以跳过。但是如果自己开发支持with
语法的对象的人需要继续阅读了解其底层细节。
对上下文管理协议的一个高等级解释是:
- 表达式求值后应该返回一个叫做”context manager”的对象。该对象必须定义了
__enter__()
和__exit__()
方法。 - 将上下文管理器的
__enter__()
方法返回的值指派给VAR,如果没有as VAR
语句时丢弃返回值。 - 执行BLOCK中的代码
- 如果BLOCK抛出异常,调用
__exit__(type, value, traceback)__
并传入异常详情(和sys.exc_info()
返回的值一样)。__exit__
方法的返回值决定是否再次抛出异常: 返回false值时会重新抛出异常,而True
的时候会抹掉异常。一般来说尽量不要抹掉异常,因为这会导致在这个对象上使用with
语法的人无法意识到执行出错了。 - 如果BLOCK没有抛出异常时
__exit__()
方法仍然会被调用,但是参数type,value,traceback都为None。
给大家举一个支持事物的数据库方法的例子(省略详细代码,只写关键部分)。
(介绍下数据库的事物:将对数据库一系列的改变打包到一个事物中。事物可以提交:意味着所有的改变都写入到数据库中,也可以回滚:意味着丢弃所有的改变,数据库将不会做任何更改。具体细节可以看数据库相关的文章)
假设存在一个代表数据库连接对象。我们的目标是使得用户可以写出如下的代码:
|
|
当区块中的代码执行正确时提交事物,遇到异常时回滚。以下是对DatabaseConnection
中基本接口的假设:
|
|
__enter__()
简单到只需要开始一个新的事物。在本应用中,cursor对象很有用,因此将它返回出去。用户可以在with
语法中使用as cursor
表达式将cursor绑定到一个变量上。
|
|
相比之下__exit__()
方法就有点复杂了,因为它要做比较多的工作。这个方法需要检查是否发生异常,如果没有异常就提交事物,否则回滚事物。
下面的代码在执行完成后会返回函数默认返回值None
,由于None
的真假值为假,所以异常会自动重新抛出来。如果你期望代码可读性更高一点,可以在标记的地方增加自己的return
语句。
|
|
contextlib 模块
新的contextlib
模块提供一些方法和装饰器来简化编写对象的with
语法支持。
装饰器叫做contextmanager()
,它允许你写一个生成器函数而不是定义一个新的类。生成器应该而且只能yield
一个值。yield
前面的代码将会当做__enter__()
方法执行,yield
出去的值会被当成该方法的返回值,然后该值会绑定到with
语法中的as
后面的变量中(如果存在的话)。yield
之后的代码会被当做__exit__()
方法执行。with
代码块中的任何异常都会被yield
语句抛出。
先前的数据库例子可以使用装饰器实现:
|
|
contextlib
模块还有一个nested(mgr1, mgr2, ...)
方法,该方法可以将多个上下文管理器绑定到一起,这样你就不需要写嵌套的with
语句了。在这个例子中,可以将开启数据库事物以及获取线程锁整合到单个with
语句中:
|
|
最后,closing(object)
方法返回一个能够绑定到as
后面的变量的对象,当with
代码块结束时会调用object.close
方法。
|
|