在Python2.6中引入了 with 语句,如以下代码
with open("myfile", "r") as f:
print f.readline()
相信很多的Python教程中都提到过 with 这么一个简洁的语法。如果不用with,那么我们将改写成如下形式:
f = file("myfile", "r")
try:
print f.readline()
execpt Exception:
pass
finally:
f.close()
一对比发现,with语言显得更加简洁,原因就是open对象实现了上下文管理协议(context manage), 既在class中实现了 __enter__ 和 __exit__ 两个方法,如果要自己实现一个支持上下文协议的对象,那么在这个对象中必须实现这两个方法。
现在来假设这么一种场景,代码中全局的日志级别为error,但是在调用某个方法的时候需要设置日志级别为debug,并且调用结束之后,日志级别仍然为error,代码和执行结果如下:
现在我们来分析一下代码。
执行with的时候,先调用 __enter__ 方法,当with中带有as 的时候,该方法返回的对象赋值给 a,然后执行with内部的代码块,此时日志级别已经调整到dubug等级,所以这里是可以打印出 "in debug"的,最后调用__exit__ 方法,注意到这里,__exit__有三个参数,exc_type, exc_val,exc_tb。通过debug打印出来,发现都是None。这是因为,只有with代码块里,发生异常时,这三个参数才会有值,exc_type 对应异常类,exc_val 对应异常值,exc_tb对应调用栈信息,这三个值刚好对应着发生异常时调用的sys.exc_info(),如下图
这意味着什么呢?
这就说明了,在with的__exit__方法中可以处理异常而不需要try...except, 下面举个例子。
如果你不想处理异常,而是想让程序忽略这个异常继续执行下去,那么只需要在 __exit__方法中return True就行了。
让我们把事情变得更复杂一点。
如果说想要一个对象支持上下文协议,那么这个对象必须实现__enter__ 和 __exit__两个方法,代码中或许不止一个对象需要用到上下文管理器,那不就要把全都的对象都实现了这两个方法,那岂不是显得很繁琐。所以问题来了,我们要让代码更加具有通用性。
首先,我们来分析一下问题,第一步是要得到一个支持上下文协议的对象,那么我们先定义一个基础类
class GenerateContextManager(object):
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
return True
基础类定义好之后,考虑一下如何去生成这个类的实例,并添加和主要功能不相关的逻辑代码
def gen_cm_instance()
return GenerateContextManager()
执行结果在这里:
那么如何添加和主要逻辑的代码呢?这里我们把生成实例的函数改成装饰器。
class Student(object):
def __init__(self):
self.student_list = [x for x in xrange(10)]
def my_log(self):
log.debug("in Student")
class GenerateContextManager(object):
def __init__(self, func):
self.func = func
def __enter__(self):
try:
return self.func.next()
except StopIteration:
raise
def __exit__(self, exc_type, exc_val, exc_tb):
try:
self.func.next()
except StopIteration:
return
return True
def gen_wrapper(func):
@wraps(func)
def wrapper(*args, **kwargs):
return GenerateContextManager(func(*args, **kwargs))
return wrapper
@gen_wrapper
def gen_cm_instance():
log.setLevel(logging.DEBUG)
log.debug("before yield")
yield Student()
log.debug("after yield")
log.setLevel(logging.ERROR)
log.debug('before with')
with gen_cm_instance() as a:
log.debug('in debug')
log.debug(a)
a.my_log()
log.debug('after with')这里是执行的结果:
大家可以看到,在gen_cm_instance() 函数中我用了yield,为什么呢?
这是因为通过yield能够把这个函数里面的代码一分为二,从而达到从主要逻辑中分离次要逻辑的目的。关于yield的用法可以去google更多的教程。
其实,我刚刚的写法就是官方标准库中contextlib提供的一种更加简单的写法,关于context
有些时候我们只是需要它去支持上下文协议,并不需要管太多__enter__ 和 __exit__ 方法中添加了什么代码。因此标准库中有更简洁的写法,那就是contextlib,这个库的代码很简单:
def contextmanager(func): """@contextmanager decorator. Typical usage: @contextmanager def some_generator(): try: yield finally: This makes this: with some_generator() as : equivalent to this: try: = finally: """ @wraps(func) def helper(*args, **kwds): return GeneratorContextManager(func(*args, **kwds)) return helper
官方代码很简单,也很精炼,而且还告诉我们具体使用方式,所以说,当我们对某个函数或者对象如何使用不熟悉的时候,不一定要去网上找教程,很多代码,官方文档中就已经解释得很详细了。
关于上下文协议的东西就讲到这里,在标准库contextlib中还有几个其他的用法,这里就不详细解释了,感兴趣的可以自己去看源代码,反正也不是很难看懂。