翻译-Python中的with语句

本文主要介绍Python 中的with语法,该语法从Python 2.5中作为__future__模块加入,在Python 2.6中加入到正式环境。

对个人来说,主要有两点收获:

  • 如何让自己的对象支持with语法
  • 发现了contextlib库

来源: What’s new in Python 2.6

正文

with语法简化了之前使用try...finally代码块来确保清理代码被执行的写法。本节会跟大家讨论下with语法的常见用法。下一节会介绍它的实现细节以及如何在自己的对象中支持with语法。

with语法作为一个新的流程控制体,下面是它的基本结构:

1
2
with expression [as variable]:
with-block

对这个表达式求值后会得到一个支持上下文管理协议(即__enter__()__exit()__)方法的对象。

对象的__enter__()方法会在with-block被执行时调用,因此可以用来执行一些预处理代码。如果表达式中写了as vaiable部分的话,这个方法同时会返回一个值,这个值会和变量名variable进行绑定。(注意: variable并不会绑定到expression表达式的结果上)。

with-block代码在执行结束后会调用对象的__exit__()方法,即使抛出了异常也不会例外。这样清理代码总是会被执行到。

在 Python 2.5中你需要使用如下代码启用with语法(之后的版本默认开启):

1
from __future__ import with_statement

Python标准库中的一些对象现在支持使用with语法来进行上下文管理。用文件对象举例说明:

1
2
3
4
with open('/etc/passwd', 'r') as f:
for line in f:
print line
... more processing code ...

当这段代码执行完后,即使for循环过程中遇到了异常,文件对象f也会被自动关闭。

注意: 在这个例子中,fopen()方法创建的对象一样,因为file.__enter()__返回的是自身self

threading模块的锁和条件变量同样支持with语法:

1
2
3
4
lock = threading.Lock()
with lock:
# Critical section of code
...

区块代码会在执行前自动获取锁,执行完毕后自动释放锁。

模块decimal中新的localcontext()函数简化了当前decimal上下文的保存和复原,这样在with的代码块中可以指定计算所需的精度以及舍入特性而不影响到外面的代码。

1
2
3
4
5
6
7
8
9
10
from decimal import Decimal, Context, localcontext
# Displays with default precision of 28 digits
v = Decimal('578')
print v.sqrt()
with localcontext(Context(prec=16)):
# All code in this block uses a precision of 16 digits.
# The original context is restored on exiting the block.
print v.sqrt()

编写上下文管理

当前的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。

给大家举一个支持事物的数据库方法的例子(省略详细代码,只写关键部分)。

(介绍下数据库的事物:将对数据库一系列的改变打包到一个事物中。事物可以提交:意味着所有的改变都写入到数据库中,也可以回滚:意味着丢弃所有的改变,数据库将不会做任何更改。具体细节可以看数据库相关的文章)

假设存在一个代表数据库连接对象。我们的目标是使得用户可以写出如下的代码:

1
2
3
4
5
db_connection = DatabaseConnection()
with db_connection as cursor:
cursor.execute('insert into ...')
cursor.execute('delete from ...')
# ... more operations ...

当区块中的代码执行正确时提交事物,遇到异常时回滚。以下是对DatabaseConnection中基本接口的假设:

1
2
3
4
5
6
7
8
class DatabaseConnection:
# Database interface
def cursor (self):
"Returns a cursor object and starts a new transaction"
def commit (self):
"Commits current transaction"
def rollback (self):
"Rolls back current transaction"

__enter__()简单到只需要开始一个新的事物。在本应用中,cursor对象很有用,因此将它返回出去。用户可以在with语法中使用as cursor表达式将cursor绑定到一个变量上。

1
2
3
4
5
6
class DatabaseConnection:
...
def __enter__ (self):
# Code to start a new transaction
cursor = self.cursor()
return cursor

相比之下__exit__()方法就有点复杂了,因为它要做比较多的工作。这个方法需要检查是否发生异常,如果没有异常就提交事物,否则回滚事物。

下面的代码在执行完成后会返回函数默认返回值None,由于None的真假值为假,所以异常会自动重新抛出来。如果你期望代码可读性更高一点,可以在标记的地方增加自己的return语句。

1
2
3
4
5
6
7
8
9
10
class DatabaseConnection:
...
def __exit__ (self, type, value, tb):
if tb is None:
# No exception, so commit
self.commit()
else:
# Exception occurred, so rollback.
self.rollback()
# return False

contextlib 模块

新的contextlib模块提供一些方法和装饰器来简化编写对象的with语法支持。

装饰器叫做contextmanager(),它允许你写一个生成器函数而不是定义一个新的类。生成器应该而且只能yield一个值。yield前面的代码将会当做__enter__()方法执行,yield出去的值会被当成该方法的返回值,然后该值会绑定到with语法中的as后面的变量中(如果存在的话)。yield之后的代码会被当做__exit__()方法执行。with代码块中的任何异常都会被yield语句抛出。

先前的数据库例子可以使用装饰器实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from contextlib import contextmanager
@contextmanager
def db_transaction (connection):
cursor = connection.cursor()
try:
yield cursor
except:
connection.rollback()
raise
else:
connection.commit()
db = DatabaseConnection()
with db_transaction(db) as cursor:
...

contextlib模块还有一个nested(mgr1, mgr2, ...)方法,该方法可以将多个上下文管理器绑定到一起,这样你就不需要写嵌套的with语句了。在这个例子中,可以将开启数据库事物以及获取线程锁整合到单个with语句中:

1
2
3
lock = threading.Lock()
with nested (db_transaction(db), lock) as (cursor, locked):
...

最后,closing(object)方法返回一个能够绑定到as后面的变量的对象,当with代码块结束时会调用object.close方法。

1
2
3
4
5
6
import urllib, sys
from contextlib import closing
with closing(urllib.urlopen('http://www.yahoo.com')) as f:
for line in f:
sys.stdout.write(line)