with语句是在python2.5开始引进的一种与异常处理相关的功能。在讲with语句之前,先说说上下文管理器,因为使用上下文管理器最广泛的就是with语句了。
基本概念
上下文管理协议(Context Management Protocol):包含方法 __enter__() 和 __exit__(),支持该协议的对象要实现这两个方法。
上下文管理器(Context Manager):支持上下文管理协议的对象,这种对象实现了__enter__() 和 __exit__() 方法。上下文管理器定义执行 with 语句时要建立的运行时上下文,负责执行 with 语句块上下文中的进入与退出操作。通常使用 with 语句调用上下文管理器,也可以通过直接调用其方法来使用。
运行时上下文(runtime context):由上下文管理器创建,通过上下文管理器的 __enter__() 和__exit__() 方法实现,__enter__() 方法在语句体执行之前进入运行时上下文,__exit__() 在语句体执行完后从运行时上下文退出。with 语句支持运行时上下文这一概念。
上下文表达式(Context Expression):with 语句中跟在关键字 with 之后的表达式,该表达式要返回一个上下文管理器对象。
语句体(with-body):with 语句包裹起来的代码块,在执行语句体之前会调用上下文管理器的 __enter__() 方法,执行完语句体之后会执行 __exit__() 方法。
上下文管理器
这里构造一个简单的上下文管理器的例子,以理解__enter__() 和__exit__() 方法。
from math import sqrt, pow
class Point(object):
def __init__(self, x, y):
print('initialize x and y')
self.x, self.y = x, y
def __enter__(self):
print("Entering context")
return self
def __exit__(self, type, value, traceback):
print("Exiting context")
def get_distance(self):
distance = sqrt(pow(self.x, 2) + pow(self.y, 2))
return distance
if __name__ == "__main__":
with Point(3,4) as pt:
print('distance ' + str(pt.get_distance()))
#output
initialize x and y
Entering context
distance 5.0
Exiting context
上面的代码定义了一个 Point 类,并实了 __enter__() 和 __exit__() 方法,我们还定义了 get_distance 方法,用于返回点到原点的距离。
通过with语句调用上下文管理器,具体执行过程如下:
<1>Point(3, 4) 生成了一个上下文管理器;
<2>调用上下文管理器的 __enter__() 方法,并将 __enter__() 方法的返回值赋给 as 字句中的变量 pt;
<3>执行语句体(指 with 语句包裹起来的代码块)内容,输出 distance;
<4>不管执行过程中是否发生异常,都执行上下文管理器的 __exit__() 方法。__exit__() 方法负责执行『清理』工作,如释放资源,关闭文件等。如果执行过程没有出现异常,或者语句体中执行了语句 break/continue/return,则以 None 作为参数调用 __exit__(None, None, None);如果执行过程中出现异常,则使用 sys.exc_info 得到的异常信息为参数调用 __exit__(exc_type, exc_value, exc_traceback);
<5>出现异常时,如果 __exit__(type, value, traceback) 返回 False 或 None,则会重新抛出异常,让 with 之外的语句逻辑来处理异常;如果返回 True,则忽略异常,不再对异常进行处理;
上面的 with 语句执行过程没有出现异常,我们再来看出现异常的情形:
if __name__ == "__main__":
with Point(3,4) as pt:
print('distance ' + str(pt.get_lengh()))
#output
initialize x and y
Entering context
Exiting context
并且有报错信息
在我们的例子中,__exit__ 方法返回的是 None(如果没有 return 语句那么方法会返回 None)。因此,with 语句抛出了那个异常。我们对 __exit__ 方法做一些改动,让它返回 True。
def __exit__(self, type, value, traceback):
print("Exiting context")
return True
#output
initialize x and y
Entering context
Exiting context
可以看到,由于 __exit__ 方法返回了 True,因此没有异常会被 with 语句抛出。
with语句是怎么执行的?
context_manager = context_expression
exit = type(context_manager).__exit__
value = type(context_manager).__enter__(context_manager)
exc = True # True 表示正常执行,即便有异常也忽略;False 表示重新抛出异常,需要对异常进行处理
try:
try:
target = value # 如果使用了 as 子句
with-body # 执行 with-body
except:
# 执行过程中有异常发生
exc = False
# 如果 __exit__ 返回 True,则异常被忽略;如果返回 False,则重新抛出异常
# 由外层代码对异常进行处理
if not exit(context_manager, *sys.exc_info()):
raise
finally:
# 正常退出,或者通过 statement-body 中的 break/continue/return 语句退出
# 或者忽略异常退出
if exc:
exit(context_manager, None, None, None)
# 缺省返回 None,None 在布尔上下文中看做是 False
<1>执行 context_expression,生成上下文管理器 context_manager
<2>调用上下文管理器的 __enter__() 方法;如果使用了 as 子句,则将 __enter__() 方法的返回值赋值给 as 子句中的 target(s)
<3>执行语句体 with-body
<4>不管是否执行过程中是否发生了异常,执行上下文管理器的 __exit__() 方法,__exit__() 方法负责执行“清理”工作,如释放资源等。如果执行过程中没有出现异常,或者语句体中执行了语句 break/continue/return,则以 None 作为参数调用 __exit__(None, None, None) ;如果执行过程中出现异常,则使用 sys.exc_info 得到的异常信息为参数调用 __exit__(exc_type, exc_value, exc_traceback)
<5>出现异常时,如果 __exit__(type, value, traceback) 返回 False,则会重新抛出异常,让with 之外的语句逻辑来处理异常,这也是通用做法;如果返回 True,则忽略异常,不再对异常进行处理。
内建对象使用with语句
除了自定义上下文管理器,Python 中也提供了一些内置对象,可直接用于 with 语句中,比如最常见的文件操作。
传统的文件操作经常使用 try/finally 的方式,比如:
file = open('somefile', 'r')try:
for line in file:
print(line)
finally:
file.close() # 确保关闭文件
将上面的代码改用 with 语句:
with open('somefile', 'r') as file:
for line in file:
print(line)
可以看到,通过使用 with,代码变得很简洁,而且即使处理过程发生异常,with 语句也会确保我们的文件被关闭。
contextlib 模块
除了在类中定义 __enter__ 和 __exit__ 方法来实现上下文管理器,我们还可以通过生成器函数(也就是带有 yield 的函数)结合装饰器来实现上下文管理器,Python 中自带的 contextlib 模块就是做这个的。
contextlib 模块提供了三个对象:装饰器 contextmanager、函数 nested 和上下文管理器 closing。其中,contextmanager 是一个装饰器,用于装饰生成器函数,并返回一个上下文管理器。需要注意的是,被装饰的生成器函数只能产生一个值,否则会产生 RuntimeError 异常。
下面我们看一个简单的例子:
from contextlib import contextmanager
@contextmanager
def point(x, y):
print('before yield')
yield x * x + y * y
print('after yield')
with point(3, 4) as value:
print('value is: %s' % value)
#output
before yield
value is: 25
after yield
可以看到,yield 产生的值赋给了 as 子句中的 value 变量。
另外,需要强调的是,虽然通过使用 contextmanager 装饰器,我们可以不必再编写 __enter__ 和 __exit__ 方法,但是『获取』和『清理』资源的操作仍需要我们自己编写:『获取』资源的操作定义在 yield 语句之前,『释放』资源的操作定义在 yield 语句之后。
总结
<1>上下文管理器是支持上下文管理协议的对象,也就是实现了 __enter__ 和 __exit__ 方法。
<2>通常,我们使用 with 语句调用上下文管理器。with 语句尤其适用于对资源进行访问的场景,确保执行过程中出现异常情况时也可以对资源进行回收,比如自动关闭文件等。
<3>__enter__ 方法在 with 语句体执行前调用,with 语句将该方法的返回值赋给 as 字句中的变量,如果有 as 字句的话。
<4>__exit__ 方法在退出运行时上下文时被调用,它负责执行『清理』工作,比如关闭文件,释放资源等。如果退出时没有发生异常,则 __exit__ 的三个参数,即 type, value 和 traceback 都为 None。如果发生异常,返回 True 表示不处理异常,否则会在退出该方法后重新抛出异常以由 with 语句之外的代码逻辑进行处理。
参考博文
http://funhacks.net/explore-python/Advanced-Features/context.html
https://www.ibm.com/developerworks/cn/opensource/os-cn-pythonwith/