上下文管理器和else块
今天的主题是讨论以下特性:
* with语句和上下文管理器
* for、while、try语句的else子句
使用with语句会设置一个临时的上下文,交给上下文管理器对象控制,并负责清理上下文。这么做主要是为了避免样子代码,易于使用。除了自动关闭文件外,with还有很多其他用途。
else子句与with语句没有任何联系。
先做这个,在做那个:if语句之外的else块
这个语言特性不是什么秘密,但却没有得到应有的重视:else子句不仅能在if语句中使用,也能在for、while和try语句中使用。
else子句的行为如下:
for
仅当for循环结束时(没有被break语句跳出),运行else块
while
仅当while循环条件为False时(没有被break跳出),运行else块
try
try块中没有异常抛出时,运行else块,且else子句抛出的异常不会被之前的except捕获。
笔者认为,else在这三个里面的意思与其真正作用有点矛盾,选择else作为关键字的确会令人有些苦恼,替换成then、final更好些。
1 for item in my_list: 2 if item == 'banana': 3 break 4 else: 5 print("over. no banana in my_list") 6 7 """ 8 如果my_list中包含'banana'那么for循环会被break跳出,从而不会打印"over. no banana in my_list“,如果my_list没有‘banana’,那么for循环正常结束,就会打印那句提示 9 """
1 try: 2 dangerous_call() 3 except OSError: 4 log('OSError...') 5 else: 6 after_call()
只有在try块不抛出异常的情况下,else块才会执行。
在python中,try/except不仅用于处理错误,还常用于控制流程。
EAFP
取得原谅比获得许可容易(easier to ask for forgiveness than permission)。这是一种常见的 Python 编程风格,先假定存在有效的键或属性,如果假定不成立,那么捕获异常。这种风格简单明快,特点是代码中有很多 try 和 except 语句。与其他很多语言一样(如 C 语言),这种风格的对立面是 LBYL 风格
LBYL
三思而后行(look before you leap)。这种编程风格在调用函数或查找属性或键之前显式测试前提条件。与 EAFP 风格相反,这种风格的特点是代码中有很多 if语句。在多线程环境中,LBYL 风格可能会在“检查”和“行事”的空当引入条件竞争。例如,对 if key in mapping: return mapping[key] 这段代码来说,如
果在测试之后,但在查找之前,另一个线程从映射中删除了那个键,那么这段代码就会失败。
上下文管理器和with块
with语句的目的是简化try/finally模式。这种模式用于执行某段代码执行完毕后的某项操作,如关闭文件、关闭管道、或者关闭socket连接等。通常用于释放资源或者还原状态。
上下文管理协议要实现__enter__方法,__exit__方法。with语句开始时会在上下文管理对象上调用__enter__方法,with语句运行结束后,会调用上下文管理对象的__exit__方法。
1 class FileContext: 2 def __init__(self, filename, mode='r'): 3 self.filename = filename 4 self.mode = mode 5 6 def __enter__(self): 7 print("__enter__") 8 self.fp = open(self.filename, self.mode) 9 return self.fp 10 11 def __exit__(self, exc_type, exc_val, tb): 12 print("__exit__") 13 print("exc_type: %s, exc_val: %s, tb: %s" %(exc_type, exc_val, tb)) 14 self.fp.close() 15 del self.fp 16 return True 17 18 with FileContext("xxxx", 'w') as f: 19 print(f) 20 time.sleep(3)
在执行with后面的表达式时,会执行__enter__方法,然后用as句法让f引用__enter__方法返回的文件对象(这里要澄清一点with后面表达式得到是上下文管理对象,这个对象调用__enter__方法得到的返回值才是f)。不管控制流程以哪种方式退出with块,__exit__都会执行。
__exit__的三个参数如下:
exc_type
异常类型,如ZeroDivisionError
exc_value
异常实例,传递给异常构造方法的参数
traceback/tb
traceback对象
__exit__方法如果返回值不是True,那么with语句中的任何异常都会向上冒泡。
contextlib模块中的实用工具
contextlib模块中有一些类和函数:
closing
如果对象提供了close()方法,但没有实现__enter__/__exit__,这个函数会自动构建上下文管理器
suppress
构建临时忽略指定异常的上下文管理器
@contextmanager
这个装饰器把简单的生成器函数编程上下文管理器,这样就不用创建类去实现管理器协议了
ContextDecorator
这是个基类,用于定义基于类的上下文管理器。这种上下文管理器也能用于装饰函数,在受管理的上下文中运行整个函数
ExitStack
这个上下文管理器能进入多个上下文管理器。with块结束时,ExitStack按照后进先出的原则一次调用栈中各个上下文管理器的__exit__方法。如果事先不知道with块要进入多少个上下文管理器,可以使用这个类。例如,同时打开任意目录下的所有文件。
这里面,使用最广泛的@contextmanager装饰器,因此要格外小心。这个装饰器很容易迷惑人,他跟迭代无关,但是使用了yield语句。
使用@contextmanager
@contextmanager装饰器能有效减少创建上下文管理器的样板代码量,我们无需编写一个类,还要实现__enter__和__exit__方法,只需实现一个带有yield语句的生成器,返回__enter__返回的值即可。
在使用@contextmanager装饰的生成器中,yield语句前的所有代码在with后的表达式中调用,yield返回的值作为as 后面的变量接收,yield以后的代码(想让__exit__完成的操作),在with块结束之后调用。
1 @contextlib.contextmanager 2 def openfile(filename, mode='r'): 3 try: 4 fp = None 5 print("openfile(%s, %s)" %(filename, mode)) 6 fp = open(filename, mode) 7 yield fp 8 except FileNotFoundError: 9 print("file not found") 10 yield fp 11 finally: 12 print("finally: ...") 13 if fp: 14 fp.close() 15 16 with openfile('xxxxy') as f: 17 print(f) 18 time.sleep(2)
为了能够正常释放文件对象,先把它定义成None,然后在finally中,判断它的值。
本质上,@contextmanager装饰器会把函数包装成为实现__enter__和__exit__方法的上下文管理类(上下文管理对象必须遵守的上下文管理协议)。
这个类的__enter__方法的作用:
1)调用生成器函数,保存生成器对象(gen)
2)调用next(gen),执行到yield关键字处暂停
3)yield返回的值绑定到as后的变量上
with语句终止时,这个类的__exit__方法做了下面几件事:
1)检查有没有异常抛出,传给它的exc_type参数,如果有,调用gen.throw(exception),在生成器函数包含yield关键字哪行抛出异常
2)如果没有异常,调用next(gen),从生成器函数上次终止的yield语句后继续执行
为了告诉解释器异常已经得到处理了,__exit__方法应该返回True,此时解释器会压制异常。如果__exit__方法没有显式返回一个值,那么解释器得到是None,然后向上冒泡异常。然而,使用@contextmanager装饰器时,默认的行为时相反的,解释器会默认所有的异常已经得到处理了,因此应该压制异常。如果不想让@contextmanager压制异常,应该在被装饰的函数中重新显式的抛出异常。
@contextmanager装饰器方便、使用,把三个不同的python特性结合到了一起,装饰器、生成器、上下文管理。
posted on 2019-03-08 17:06 forwardFields 阅读(168) 评论(0) 编辑 收藏 举报