最近组里在CoDe(Competence Development,能力培养)。花姐讲文件操作,例子里有如下的代码:
with open(r'demo.txt') as f:
for line in f:
print line
# ...more code
语句很简单,就是把demo.txt文件打开,逐行打印出来。我感兴趣的是这里的with语句,这个用法很简洁,比下面的try … except 用法要少一些代码量。
f = open(r'demo.txt')
try:
for line in f:
print line
# ...more code
finally:
f.close()
可以看到,下面的这种用法代码稍微复杂一些,还涉及到了文件的关闭操作。而with语句是怎么避免了显性的关闭文件呢?更进一步,with这个语句是怎么实现的,它的目的和好处是什么呢?
对比着看,可以感觉到with做了一系列的操作,准备工作、用户代码、扫尾工作。准备工作就是打开文件并且返回文件句柄(是不是这么定义?),用户代码就是中间那一部分逐行打印。扫尾工作就是关闭文件。
再做一个类比。在用robotframework做自动化测试时,总会定义 suite setup 和 suite teardown,这就是保证测试的进入和退出都有一定的保护措施,进入时做准备,退出时打扫战场。Python 的 with 语句就是suite setup 和 suite teardown的作用,是Python 2.5以后引入的特性。
在Python 2.5里要通过
from __future__ import with_statement
才能使用with 语句,到了Python 2.6就可以直接用了,它从一个实验性的特性变成了内置的了。
直接了当说,with的原理是实现了一个上下文管理协议(Context Management Protocol),而这个协议的两个必要部分就是两个方法,__enter__() 和 __exit__(),任何支持该协议的对象要实现这两个方法。一目了然了吧,这两个方法可以对应robotframework里的suite setup 和 suite teardown。
先看一个例子吧。
class DummyResource(object):
def __init__(self, tag):
self.tag = tag
print 'Resource [%s]' % tag
def __enter__(self):
print '[Enter %s]: Allocate resource.' % self.tag
return self
def __exit__(self, exc_type, exc_value, exc_tb):
print '[Exit %s]: Free resource.' % self.tag
if exc_tb is None:
print '[Exit %s]: Exited without exception.' % self.tag
else:
print '[Exit %s]: Exited with exception raised.' % self.tag
return False
这段代码我是从最顶上那个帖子里抄的。可以做一些说明。
- 这是一个叫DummyResource的类,提供了__enter__() 和 __exit__()两个方法,说明这个类是可以用with语句来调用的。另外从类名我们可以感觉到,with语句的运用都是和资源相关的。什么是资源?文件,网络套接字,线程thread,等等这些都是资源。
- __enter__()方法里,除了一句打印“山顶的朋友,我在这里”,就是一句返回语句:
class DummyResource(object):
# ...
def __enter__(self):
print '[Enter %s]: Allocate resource.' % self.tag
return self
# ...
返回了一个self!这是什么意思?就是把这个类的实例返回出去。这样
with context_expression [as target(s)]:
with-body
中的as target(s)就可以得到这个值了。忘了说,target(s)之所以有或许复数,是因为__enter__()方法是可以返回多个值成为一个元组的。
- __exit__() 是个更复杂的方法,用来定义退出时候的动作。在方法的定义中可以看到有额外的参数exc_type、exc_value 和 exc_tb。这三个都是和异常相关的。如果with语句包住的用户代码正常执行,那么这三个变量都是None;反之,如果出错了,可以简单通过exc_type来判断异常的类型,配合后面两个变量获得更多的异常信息,进一步做相应的处理。
- __exit__()方法中更重要的是它的返回值。return False。False的意思是“我还没有处理完,我要把这个异常继续抛出去,你们外面的代码去抓住它继续处理吧”。如果心满意足的,或者心怀鬼胎的觉得已经处理完了,就返回个真值,外面的代码就不会知道有异常发生过。很多问题的根本原因就这么湮没了。:-(
结合这个类定义,看看实用的调用例子吧。
小乖乖:
with DummyResource('Normal'):
print '[with-body] Run without exceptions.'
和不乖的:
with DummyResource('With-Exception'):
print '[with-body] Run with exception.'
raise Exception
print '[with-body] Run with exception. Failed to finish statement-body!'
小乖乖的输出是这样的:
Resource [Normal]
[Enter Normal]: Allocate resource.
[with-body] Run without exceptions.
[Exit Normal]: Free resource.
[Exit Normal]: Exited without exception.
很好,按部就班,只要读得懂类代码就很容易看出这些输出。下面看一下“小淘气”的输出呢。
Resource [With-Exception]
[Enter With-Exception]: Allocate resource.
[with-body] Run with exception.
[Exit With-Exception]: Free resource.
[Exit With-Exception]: Exited with exception raised.
Traceback (most recent call last):
File "G:/demo", line 20, in <module>
raise Exception
Exception
除了读代码能看清的输出,还要说一两句。
- 用户代码中是先抛出了异常后又打印了一句“[with-body] Run with exception. Failed to finish statement-body!”。在输出中这句没有打印。这说明了__exit__()方法是清道夫,你们用户代码怎么乱糟糟,你先跑,抛出异常了,就别接着往下走了,我留下来打扫战场。
- 输出中的“Traceback (most recent call last):”一段是解释器抛出的异常,在__exit__()方法中的开关就是最后的那个返回否值。如果是返回真值,这段Traceback是不会输出的。
最后,说说最初的那个花姐举出来的例子。
with open(r'demo.txt') as f:
for line in f:
print line
# ...more code
打开一个Python的IDE(我用的是Pycharm CE),可以点到open的定义,就是一句话。
def open(name, mode=None, buffering=None): # real signature unknown; restored from __doc__
"""
open(name[, mode[, buffering]]) -> file object
Open a file using the file() type, returns a file object. This is the
preferred way to open a file. See file.__doc__ for further information.
"""
return file('/dev/null')
仅仅返回一个file实例。继续点过去,就豁然开朗了。
class file(object):
# ... some codes
def __enter__(self): # real signature unknown; restored from __doc__
""" __enter__() -> self. """
return self
def __exit__(self, *excinfo): # real signature unknown; restored from __doc__
""" __exit__(*excinfo) -> None. Closes the file. """
pass
# ... more codes
file 这个类 实现了__enter__()和__exit__()方法,那外面当然可以用with语句来建立文件操作的上下文。
其实实现上下文管理协议并不是每次都需要写一个类实现两个办法,还有其他的方法,会另开一篇写写。
-EOF-