谈一谈Python的上下文管理器
经常在Python代码中看到with语句,仔细分析下,会发现这个with语句功能好强,可以自动关闭资源。这个在Python中叫上下文管理器Context Manager。那我们要怎么用它,什么时候用它呢。这里我们就来聊一聊。
上下文管理器的作用
很多情况,当我们使用完一个资源后,我们需要手动的关闭掉它,比如操作文件,建立数据库连接等。但是,在使用资源的过程中,如果遇到异常,很可能错误被直接抛出,导致来不及关闭资源。所以在大部分程序语言里,我们使用”try-finally”语句来确保资源会关闭。比如下面的Python写文件代码:
try:
f = open('test.txt', 'a+')
f.write('Foo\n')
finally:
f.close()
这样做固然没有问题,但是当”try-finally”中间的逻辑复杂,而且还带有各种嵌套的话,代码就很不容易维护。Python的with语句,可以说功能同上面的”try-finally”几乎一样,但代码看上去简洁的多,我们来实现同样的功能:
with open('test.txt', 'a+') as f:
f.write('Foo\n')
with语句后面跟着open()
方法,如果它有返回值的话,可以使用as
语句将其赋值给f
。在with语句块退出时,f.close()
方法会自动被调用,即使f.write()
出现异常,也能确保close()
方法被调用。
自定义类来使用上下文管理器
上例中open()
方法是Python自带的,那我们怎么定义自己的类型来使用with语句呢。其实只要你的类定义了__enter__()
和__exit__()
方法,就可以使用Python的上下文管理器了。__enter__()
方法会在with语句进入时被调用,其返回值会赋给as
关键字后的变量;而__exit__()
方法会在with语句块退出后自动被调用。
我们来实现个跟上节一样的文件写入功能:
class OpenFileDemo(object):
def __init__(self, filename):
self.filename = filename
def __enter__(self):
self.f = open(self.filename, 'a+')
return self.f
def __exit__(self, exc_type, exc_val, exc_tb):
self.f.close()
with OpenFileDemo('test.txt') as f:
f.write('Foo\n')
异常处理
肯定有朋友注意到上面的__exit__()
带了三个参数,是的,他们是用来异常处理的。大部分情况下,我们希望with语句中遇到的异常最后被抛出,但也有时候,我们想处理这些异常。__exit__()
方法中的三个参数exc_type
, exc_val
, exc_tb
分别代表异常类型,异常值,和异常的Traceback。当你处理完异常后,你可以让__exit__()
方法返回True,此时该异常就会不会再被抛出。比如我们将上例中的__exit__()
方法改一下:
def __exit__(self, exc_type, exc_val, exc_tb):
self.f.close()
if exc_type != SyntaxError:
return True
return False # Only raise exception when SyntaxError
现在,如果遇到SyntaxError
的话,异常会被正常抛出,而其他异常的话都会被忽略。
contextlib模块
Python中还有一个contextlib
模块提供一些简便的上下文管理器功能。
closing()方法
如果说with语句块在退出时会自动调用__exit__()
方法的话,那用了contextlib.closing()
的with语句块则在退出时会自动调用close()
方法。看一下示例:
import contextlib
class Resource(object):
def open(self):
print 'Open Resource'
def close(self):
print 'Close Resource'
with contextlib.closing(Resource()) as r:
r.open()
程序运行后,会打印出
Open Resource
Close Resource
说明Resource
类创建的对象被赋给了as
关键字后面的变量r
,而with语句块退出时,自动调用了r.close()
方法。
contextmanager装饰器
@contextlib.contextmanager
是一个装饰器,由它修饰的方法会有两部分构成,中间由yield
关键字分开。由此方法创建的上下文管理器,在代码块执行前会先执行yield
上面的语句;在代码块执行后会再执行yield
下面的语句。看个例子比较容易明白:
import contextlib
import time
@contextlib.contextmanager
def timeit():
start = time.time()
yield
end = time.time()
usedTime = (end - start) * 1000
print 'Use time %d ms' % usedTime
with timeit():
time.sleep(1)
这个timeit()
方法实现了一个计时器,它会计算由他生成的with语句块执行时间。可以看出,yield
上面的语句就如同之间介绍过的__enter__()
方法,而yield
下面的语句就如同__exit__()
方法。而yield
部分就是with
语句块中的代码。如果yield
后面带参数的话,我们就可以用as
关键字赋值给后面的变量,比如上例:
@contextlib.contextmanager
def timeit():
start = time.time()
yield start
#...
with timeit() as starttime:
print starttime
#...
需要注意的是,@contextlib.contextmanager
不像之前介绍的__exit__()
方法,遇到异常也会执行。也就是with语句块抛出异常的话,yield
后面的代码将不会被执行。所以,必要时你需要对yield
语句使用”try-finally”。