函数-上下文器法。
上下文管理协议(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__() 方法。
01 with语句
with是从Python2.5引入的一个新的语法,它是一种上下文管理协议的具体实现,目的在于从流程图中把 try、except 和finally 关键字和 资源分配释放相关代码统统去掉,简化try….except….finlally的处理流程。
with通过__enter__方法初始化,然后在__exit__中做善后以及处理异常。 所以使用with处理的对象必须有__enter__()和__exit__()这两个方法。
其中__enter__()方法在语句体(with语句包裹起来的代码块)执行之前进入运行,exit()方法在语句体执行完毕退出后运行。
with 语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的“清理”操作,释放资源,比如文件使用后自动关闭、线程中锁的自动获取和释放等。
With语句的基本语法格式:
with expression [as target]: with_body ''' 参数说明: expression:是一个需要执行的表达式; target:是一个变量或者元组,存储的是expression表达式执行返回的结果,可选参数。 with_body:是一段语句体。 '''
02 应用场景
有一些任务,可能事先需要设置,事后做清理工作。对于这种场景,Python的with语句提供了一种非常方便的处理方式。一个很好的例子是文件处理,用户需要获取一个文件句柄,从文件中读取数据,然后关闭文件句柄。
- 文件操作
- 进程线程之间互斥对象
- 资源的加锁和解锁
- 支持上下文其他对象
03 示例
# 不用with语句打开文件 f = open("test.txt") print(f.read()) f.close() ''' 这里有两个问题: 一是可能忘记关闭文件句柄; 二是文件读取数据发生异常,没有进行任何处理。 ''' # 使用异常处理语句增强文件操作 try: f = open('test.txt') print(f.read()) except: print('fail to open') finally: f.close() ''' 这里有一个问题: 虽然这段代码运行良好,但是太冗长 ''' # 使用with上下文管理器 with open('test.txt') as f: print(f.read()) ''' 有更优雅的语法, 还可以很好的处理上下文环境产生的异常。 ''' with open('test.txt') as f: print(f.read()) print(f.closed) # 返回True,说明文件句柄已经被自动关闭
04 with工作原理
紧跟with关键字后面的表达式会被求值,表达式运算之后返回一个对象,该对象必须包含两个方法:enter()和__exit__()。表达式运算之后,会调用对象的__enter__()方法,这个方法的返回值将被赋值给as关键字后面的变量。
当with包含的代码块全部被执行完之后,再调用前面返回对象的__exit__()方法。
针对上面示例,当打开文件执行操作时,具体流程如下:
- with语句先暂存了File类的__exit__⽅法
- 然后它调⽤File类的__enter__⽅法
- __enter__⽅法打开⽂件并返回给with语句
- 打开的⽂件句柄被传递给 f 参数
- 我们使⽤.read()来读取⽂件
- with语句调⽤之前暂存的__exit__⽅法
- __exit__⽅法关闭了⽂件
#示例1,演示上下文运行全流程: class Test: def __enter__(self): print("开始运行上下文") def __exit__(self, type, value, trace): print("结束运行上下文") with Test(): print("上下文运行中")
#示例2,如果with表达式运算结果不是上下文管理器,with将抛出异常: class Test: pass with Test(): print("上下文运行中") ''' Traceback (most recent call last): File "d:/www_vs/test2.py", line 8, in <module> with test(): AttributeError: __enter__ '''
#示例3,完善上下文管理器 class Test: name = "上下文管理器" def __enter__(self): return self def __exit__(self, type, value, trace): print(type) print(value) print(trace) with Test() as t: print(t.name) raise ''' 上下文管理器 <class 'RuntimeError'> No active exception to reraise <traceback object at 0x02BA7988> Traceback (most recent call last): File "d:/www_vs/test3.py", line 13, in <module> raise RuntimeError: No active exception to reraise '''
通过示例3,可以看到__enter__()方法被执行, enter()方法必须包含1个位置参数。该方法的返回值是this,赋值给变量t。
t表示Test的实例对象,执行with代码块,打印实例对象的属性name。然后,使用raise主动抛出异常。
exit()方法被调用。该方法必须包含4个位置参数。with真正强大之处是它可以处理异常。可能你已经注意到__exit__方法后面三个参数:type、 val 和 trace。 这些参数在异常处理中相当有用,type表示异常类型,val表示异常的具体信息,trace表示具体错误细节。
实际上,如果在with后面的代码块抛出任何异常时,exit() 方法被执行,与之关联的type、value和stack trace传给 exit() 方法,因此抛出的异常信息会被打印出来了。
在开发库时,清理资源、 关闭文件等等操作,都可以放在 exit 方法中完成。另外,exit 还可以进行异常的监控和处理。
注意,如果要跳过一个异常,只需要__exit__返回True即可。如果返回False,将抛出异常。
# 下面示例将跳过所有的TypeError,而让其他异常正常抛出。 class Test: name = "上下文管理器" def __enter__(self): return self def __exit__(self, type, value, trace): return isinstance(value, TypeError) with Test() as t: print(t.name) raise TypeError()
总之,with-as表达式极大的简化了每次写finally的工作,这对保持代码的优雅性是有极大帮助的。如果有多个项,还可以如下面方式进行编写。
with open("test1.txt", "w") as f1, open('test2.txt', "w") as f2: f1.write("test1") f2.write("test2")
因此,Python的with语句提供了一个有效的机制,让代码更简练,同时在异常产生时,清理工作更简单。
当异常发⽣时,with语句会采取哪些步骤。
- 它把异常的type,value和traceback传递给__exit__⽅法。
- 它让__exit__⽅法来处理异常。
- 如果__exit__返回的是True,那么这个异常就被优雅地处理了。
- 如果__exit__返回的是True以外的任何东西,那么这个异常将被with语句抛出。
05 应用
自定义打开文件
class myOpen(object): def __init__(self,filename): self.handle=open(filename) print( "Resource:%s"%filename ) def __enter__(self): print( "[enter %s]: 分配资源."%self.handle ) return self.handle # 可以返回不同的对象 def __exit__(self,exc_type,exc_value,exc_trackback): print( "[exit %s]: 释放资源." %self.handle ) if exc_trackback is None: print( "[exit %s]: 无异常退出."%self.handle ) self.handle.close() else: print( "[exit %s]: 包含异常的退出."%self.handle ) return False # 可以省略,缺省的None也是被看做是False with myOpen('test1.txt') as fp: for line in fp.readlines(): print( line , " " ) ''' 输出结果: Resource:test1.txt [enter <_io.TextIOWrapper name='test1.txt' mode='r' encoding='cp936'>]: 分配资源. 1 2 [exit <_io.TextIOWrapper name='test1.txt' mode='r' encoding='cp936'>]: 释放资源. [exit <_io.TextIOWrapper name='test1.txt' mode='r' encoding='cp936'>]: 无异常退出. '''
myOpen中的__enter__() 返回的是自身的引用,这个引用可以赋值给 as 子句中的fp变量; 返回值的类型可以根据实际需要设置为不同的类型,不必是上下文管理器对象本身。
exit() 方法中对变量exc_trackback进行检测,如果不为 None,表示发生了异常,返回 False 表示需要由外部代码逻辑对异常进行处理; 如果没有发生异常,缺省的返回值为 None,在布尔环境中也是被看做 False,但是由于没有异常发生,exit() 的三个参数都为 None,上下文管理代码可以检测这种情况,做正常处理。exit()方法的3个参数,分别代表异常的类型、值、以及堆栈信息。
自定义上下文类
class Mycontex(object): def __init__(self,name): self.name=name def __enter__(self): print("进入enter") return self def do_self(self): print(self.name) def __exit__(self,exc_type,exc_value,traceback): print("退出exit") print(exc_type,exc_value) with Mycontex('test') as mc: mc.do_self() ''' 输出结果: 进入enter test 退出exit None None '''
自定义类必须包含上述几个方法才能正确使用with关键字。即使程序发生了错误,Python解释器终止了程序,但是我们的类 还是顺利关闭了。
06 基于⽣成器的实现
还可以⽤装饰器(decorators)和⽣成器(generators)来实现上下⽂管理器。
Python有个contextlib模块专门⽤于这个⽬的。我们可以使⽤⼀个⽣成器函数来实现⼀ 个上下⽂管理器,⽽不是使⽤⼀个类。
from contextlib import contextmanager @contextmanager def open_file(name): f = open(name, 'w') yield f f.close()
这个实现⽅式看起来更加直观和简单。然⽽,这个⽅法需要关于⽣成器、yield 和装饰器的⼀些知识。在这个例⼦中我们还没有捕捉可能产⽣的任何异常。它的⼯作⽅式 和之前的⽅法⼤致相同。
简单剖析下这个⽅法。
- Python解释器遇到了yield关键字。因为这个缘故它创建了⼀个⽣成器⽽不是⼀个 普通的函数。
- 因为这个装饰器,contextmanager会被调⽤并传⼊函数名(open_file)作为 参数。
- contextmanager函数返回⼀个以GeneratorContextManager对象封装过的 ⽣成器。
- 这个GeneratorContextManager被赋值给open_file函数,我们实际上是在 调⽤GeneratorContextManager对象。
那现在我们既然知道了所有这些,我们可以⽤这个新⽣成的上下⽂管理器了,像这样:
with open_file('some_file') as f: f.write('hola!')