目录
基本概念
上下文管理协议(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__() 方法。
关于 Python 中 with 语句的详细说明:PEP 343
with 语句用上下文管理器定义的方法包裹一段代码的执行,等价于简单版的try...except...finally语句。with语句的主要针对的情境:不论一个代码块的执行过程是否出现异常,都要在结束的时候执行一些操作(比如清理)。
with语句语法:
with_stmt ::= "with" with_item ("," with_item)* ":" suite with_item ::= expression ["as" target]
或:
with expression [as variable]: with-block
expression应该返回一个支持“上下文管理协议”的对象,如果 as 分句存在的话,这个对象的返回值会被赋给 as 后面的变量名。
第一种语法表示中with语句的执行流程:
*注
with 语句确保只要 __enter__() 正常返回,那么 __exit__()一定会被调用。因此如果在将值赋给目标别名时发生错误,该错误将被当做发生在suite中。可以看下面的第6步。
5. with语句中嵌套的 suite 部分被执行(第二种表示中的 with-block 部分);
6. 上下文管理器的 __exit__() 方法被调用,如果是异常造成 suite(with-block) 部分退出,异常的类型、值和回溯都被当做参数传给 __exit__(type, value, traceback) 方法,这三个值和sys.exc_info的返回值相同。如果suite(with-block) 部分没有抛出异常,__exit__()的三个参数都是 None。
如果 suite 部分是由于异常导致的退出,且__exit__()方法的返回值是false,异常将被重举;如果返回值是真,异常将被终止,with 语句后的代码继续执行。
如果 suite 部分是由于不是异常的其他原因导致的退出,__exit__()方法的返回值被忽视,执行在退出发生的地方继续。
单个上下文管理器示例:
with open(r'C:\misc\data') as myfile: for line in myfile: print(line) #...more code...
Python中的文件对象包含会在with代码段后自动关闭文件对象的上下文管理器,所以即便 for 循环中抛出处理文件对象时的异常,也能保证 myfile 引用的对象被正常关闭。
如果这段代码使用 try...finally...语句改写:
myfile = open(r'C:\misc\data') try: for line in myfile: print(line) #...more code... finally: myfile.close()
可见使用 with 语句能够简化代码。
多个上下文管理器被处理的方式就像多个 with 语句嵌套执行一样:
with A() as a, B() as b: suite
等价于:
with A() as a: with B() as b: suite
多个上下文管理器示例:
with open('data') as fin, open(''res', 'w') as fout: for line in fin: if 'some key' in line: fout.write(line)
二、上下文管理器类型
context manager 是Python中 with 语句执行时用来定义运行时上下文的对象,上下文管理器控制着 进 / 出 运行时上下文的功能,上下文管理器通常由 with 语句触发,也可以直接通过调用他们的方法来使用他们。上下文管理器的通常用于保存和恢复各式各样的全局状态、加解锁资源和关闭打开的文件等等。
Python的 with 语句支持由上下文管理器定义的运行时上下文,由两个方法来实现,这两个方法允许用户在自定义的类中定义运行时上下文,执行流程在 with 语句开始前进入上下文,当 with 语句结束后退出。
上下文管理器必须提供一对方法:
contextmanager.__enter__()
进入运行时上下文,要么返回该对象,要么返回一个与运行时上下文相关的对象,如果有 as 分句的话,with 语句将会把该方法的返回值和 as 分句指定的目标(别名)进行绑定。
比如文件对象在__enter__()里返回自己,这样 open() 函数可以被当做环境表达式在一个 with 语句中使用。
Note that there is no specific slot for any of these methods in the type structure for Python objects in the Python/C API. Extension types wanting to define these methods must provide them as a normal Python accessible method. Compared to the overhead of setting up the runtime context, the overhead of a single class dictionary lookup is negligible.
自定义上下文管理器示例:
class TraceBlock(object): def message(self, arg): print('running' + arg) def __enter__(self): print('starting with block') return self def __exit__(self, exc_type, exc_value, exc_tb): if exc_type is None: print('exited normally\n') else: print('raise an exception! ' + str(exc_type)) return False #Propagate if __name__ == '__main__': with TraceBlock() as action: action.message('test1') print('reached') with TraceBlock() as action: action.message('test2') raise TypeError print ('not reached')
采用“二、上下文管理器类型”中说明的传统方法即编写一个包含__enter__()和__exit__()方法的类来创建上下文管理器并不难。不过有些时候,对于很少的上下文来说,完全编写所有代码会是额外的负担。在这些情况下,可以使用装饰器 contextlib.contextmanager() 将一个生成器函数转换为上下文管理器。
contextlib模块提供对 with 语句的支持:
contextlib.contextmanager(func)
装饰器,用来装饰一个生成器函数,使其成为一个上下文管理器。通过该装饰器可以方便地定义上下文管理器,不必创建一个类或单独指定__enter__() 和 __exit__() 方法。
示例
(该例子不应该用于实际生成HTML!):
from contextlib import contextmanager @contextmanager def tag(name): print "<%s>" % name yield print "</%s>" % name >>> with tag("h1"): ... print "foo" ... <h1> foo </h1>
上下文管理器的 __enter__() 和 __exit__() 方法由 @contextmanager 负责提供,不再是之前的迭代子。被装饰的生成器函数只能产生一个值,否则会导致异常 RuntimeError;如果使用了 as 子句的话,产生的值会赋值给 as 子句中的 target。
生成器函数中 yield 之前的语句在 __enter__() 方法中执行;yield 之后的语句在 __exit__() 中执行;yield 产生的值赋给 as 子句中的 variable 变量。
生成器 yield 的地方,切换到执行 with 语句中的代码块(with-block),生成器在代码块退出后继续执行。
如果 with 代码块中有未被处理的异常,它会被重举到生成器函数中的 yield 处,因此可以在生成器函数中使用 try...except...finally 语句来处理被重举的异常。
如果捕获异常的目标是记录日志或执行一些操作(而不是抑制它),生成器必须重举异常。否则生成器上下文管理器将会告知 with 语句说异常已经被处理了,但是一旦执行到 with 语句后的代码,异常会立即继续。
需要注意的是,@contextmanager 只是省略了 __enter__() / __exit__() 的编写,实际意义是减少了代码量。
但装饰器@contextmanager并不负责资源的“获取”和“清理”工作(需要开发者自己实现)。“获取”操作需要定义在 yield 语句之前,“清理”操作需要定义 yield 语句之后,这样 with 语句在执行 __enter__() / __exit__() 方法时会执行这些语句以获取/释放资源,即生成器函数中需要实现必要的逻辑控制,包括资源访问出现错误时抛出适当的异常。
示例:
from contextlib import contextmanager @contextmanager def make_context() : print 'enter' try : yield {} except RuntimeError, err : print 'error' , err finally : print 'exit' with make_context() as value : print value
with-block中抛出的异常将被重举到生成器的 yield 处,在生成器中可以捕获该异常。
contextlib.closing(thing)
返回一个上下文管理器,在完成代码块的执行时关闭参数 thing。等价于:
from contextlib import contextmanager @contextmanager def closing(thing): try: yield thing finally: thing.close()
可以这么使用:
from contextlib import closing import urllib with closing(urllib.urlopen('http://www.python.org')) as page: for line in page: print line
该例中,不用显式地关闭 page。即使发生错误,也会在 with 代码块退出时执行 page.close()