目录
前言
本章讨论python中的流程控制特性:
- with语句和上下文管理器,with语句会设置一个临时的上下文,交给上下文管理器对象控制,并且负责清理上下文。
- for、while、try语句的else子句。
一、if语句之外的else块
else子句不仅能在if语句中使用,还能在for、while、try语句中使用,但在这些语句中使用时与if/else中的语义差别很大:
- for/else。仅当for循环运行完毕时(即for循环没有被break语句中止),才运行else模块。
- while/else。仅当while循环因为循环条件为假值而退出时(即while循环没有被break语句中止),才运行else块。
- try/else。仅当try块中没有异常抛出时才运行else块,且else子句抛出的异常不会由前面的except子句处理。
在所有情况下,只要异常或者return、break、continue语句导致控制权跳到了复合语句的主块之外,else子句也会被跳过。
for/else
如下例子,如果找到了'banana',则break会导致控制权跳出for循环,则else子句不会执行。
try/else
不使用else的情况:
上述代码中只有当dangerous_call()不抛出异常的时候,after_call才会执行,但是这样代码结构不够清晰,try块中应该只放有可能抛出异常的语句,因此像下面这样用try/else版本更好:
现在这样很明确,try块防守的是dangerous_call可能出现的错误,而不是防守after_call。而且逻辑正确,只有当dangerous_call不抛出异常时,才会执行after_call()。
两种变成风格:EAFP与LBYL
EAFP:easier to ask for forgiveness than permission,取得原谅比获得许可容易。即先假定可以正常执行,如果假定不成立,即抛出了异常,那么捕获异常进行处理。需要使用很多的try/except。
LBYL,look before you leap,三思而后行。通常用大量的if语句进行判断,判断成功才执行。
二、上下文管理器和with块
上下文管理器对象存在的目的是管理with语句,就像迭代器对象存在的目的是为了管理for语句。
with语句的目的是简化try/finally模式。try/finally模式用于保证try模块中代码运行完毕后执行finally模块中的操作,即便try模块中代码由于异常、return语句或sys.exit()调用而中止,也会执行finally模块中的操作。finally子句中的代码通常用于释放重要的资源,或者还原临时变更的状态。
上下文管理器协议包含__enter__和__exit__两个方法,只要实现了__enter__和__exit__这两个方法,就能作为with语句块中的上下文管理器对象使用。with语句开始运行时,会在上下文管理器对象上调用__enter__方法。with语句运行结束后,会在上下文管理器对象上调用__exit__方法,__exit__方法来扮演finally子句的角色。
最常见的例子就是确保关闭文件对象。
- fp绑定到打开的文件对象上,因为文件的__enter__方法返回self。
- 从fp中读取一些数据。
- fp变量仍然可用,即与函数和模块不同,with块并没有定义新的作用域。
- 可以读取fp对象的属性。
- 但是不能在fp上执行I/O操作,因为在with块的末尾,调用TextIOWrapper.__exit__方法把文件关闭了。
标号1处,执行with语句后边的open('mirror.py')得到的结果是上下文管理器对象(这里就是文件对象,本质是TextTOWrapper类的实例(open()函数返回类型),其__enter__方法返回self),在上下文管理器对象上调用__enter__方法使得文件对象本身被绑定在了as子句中的fp上,因为文件对象的__enter__方法返回的是self。即with把with块对象的__enter__方法的返回值绑定到as自己的fp上。
不管控制流程以哪种方式退出with块,都会在上下文管理器对象(上边例子中即open函数返回的TextIOWrapper对象)调用__exit__方法,而不是在__enter__方法返回的对象上调用(但是上边例子中上下文管理器对象与__enter__返回的对象是同一个)。
with语句的as子句是可选的,对open函数来说,必须加上as子句,以便获取文件的引用。不过有些上下文管理器会返回None,因为没什么有用的对象能提供给用户。
下边例子在一个精心制作的上下文管理器对象上执行操作,以此强调上下文管理器与__enter__方法返回的对象之间的区别。
- 上下文管理器是LookingGlass类的实例,python在上下文管理器上调用__enter__方法,把返回结果绑定到what上。
- 打印一个字符串值,然后打印一个what变量的值。
- 打印出的内容是反向的,两个print语句打印的都是反向的。
- 现在with块已经执行完毕,可以看出,__enter__方法返回的值,即存储在what变量中的值,是字符串‘JABBERWOCKY’。
- 现在用print打印也不再是反向的。
下面是LookingGlass上下文管理类的代码实现:
- 除了self之外,python调用__enter__方法时不传入其他参数。
- 把sys.stdout.write方法保存在实例属性self.original_write中,以供后边使用。
- 为sys.stdout.write打猴子补丁,替换成自己编写的方法。
- 返回‘JABBERWOCKY’字符串,用于存入目标变量what。
- 用于打猴子补丁的方法,返回来打印。
- 如果一切正常,python调用__exit_-方法时传入的参数是None,None,None,如果抛出了异常,这三个参数是异常数据。
- 重复代入模块不会消耗很多资源,因为python会缓存导入的模块。
- 还原成原来的sys.stdout_write方法。
- 如果exc_type不是None,即捕获到了异常,且是ZeroDivisionError类型,则打印错误消息...
- 然后返回True,告诉解释器,异常已经被处理。
- 如果__exit__方法返回None,或者True之外的值,with块中的任何异常都会向上冒泡。
解释器调用__enter__方法时,除了隐式的self之外,不会传入任何参数,传给__exit__方法的三个参数如下:
- exc_type,异常类,例如ZeroDivisionError。
- exc_value,异常实例,有时会有参数传给异常构造方法,例如错误消息,这些参数可以使用exc_value.args获取。
- traceback,trackback对象。
下面在with块之外测试上下文管理类,需要我们手动调用__enter__和__exit__方法:
- 实例化一个上下文管理器对象。
- 调用上下文管理器对象的__enter__方法,这个方法会用逆序打印来接管标准输出。并返回一个字符串给变量monster。
- monster字符串的值判断是对的,但是打印出的True是逆序的,因为调用__enter__的时候标准输出已经被打补丁接管了。
- 调用上下文管理器对象的__exit__方法,还原标准输出。
三、contextlib模块中的使用工具
contextlib模块提供一些使用工具,帮助创建上下文管理器类。
- closing,如果对象提供了close()方法,但是没有实现__enter__/__exit__协议,那么可以使用这个函数构建上下文管理器。
- suppress,构建临时忽略指定异常的上下文管理器。
- @contextmanager,这个装饰器把简单的生成器函数变成上下文管理器,这样就不用创建类去实现管理器协议了。
- ContexDecorator,这是个基类,用于定义基于类的上下文管理器,这种上下文管理器也能用于装饰函数,在受管理的上下文中运行整个函数。
- ExitStack,这个上下文管理器能进入多个上下文管理器,with块结束时,ExitStack按照后进先出的顺序调用栈中各个上下文管理器的__exit__方法。如果事先不知道with块要进入多少个上下文管理器,可以使用这个类,比如同时打开任意一个文件列表中的所有文件。
这些工具中使用最广泛的是@contextmanager装饰器,其有迷惑人的一面,即它与迭代无关,却要使用yield语句,由此可以引出协程。
四、使用@contextmanager
@contextmanager装饰器能减少创建上下文管理器类的代码量,因为不用编写一个完整的类去实现__enter__和__exit__方法,而只需实现一个有yield语句的生成器,生成想让__enter__方法返回的值。
在使用@contextmanager装饰的生成器中,yield语句的作用是把函数的定义体分为两部分:
- yield语句前面的所有代码在with块开始,即解释器调用__enter__方法时执行。
- yield语句后边的代码在with块结束时,即调用__exit__方法时执行。
- 应用contextmanager装饰器。
- 保存原来的sys.stdout.write方法。
- 定义自定义的reverse_write方法。
- 把sys.stdout.write替换成reverse_write。
- 产出一个值,即__enter__方法的返回值,这个值会绑定到with语句中as子句的目标变量上,执行with块中的代码时,这个函数会在这一点暂停。
- 恢复标准输出,这里是__exit__方法的代码,当控制权一旦跳出with块,就会继续执行yield语句之后的代码。
实际上,contextlib.contextmanager装饰器会把函数包装成实现了__enter__和__exit__方法的类。
类的名称是_GnegeratorContextManager,其__enter__方法有如下作用:
- 调用生成器函数,保存生成器对象gen。
- 调用next(gen),执行到yield关键字所在的位置。
- 返回next(gen)产出的值,以便把产出的值绑定到with/as语句中的目标变量上。
with块终止时,__exit__方法会做以下几件事:
- 检查有没有异常传给exc_type,如果有,调用gen.throw(exception),在生成器函数定义体中包含yield关键字的那一行抛出异常。
- 否则,调用next(gen),继续执行生成器函数定义体中yield语句之后的代码。
上方使用生成器实现的上下文管理器有一个严重错误:如果在with块中抛出了异常,python解释器会将其捕获,然后在looking_glass函数的yield表达式里再次抛出。但是,哪里没有处理错误的代码,因此looing_glass函数会终止,永远无法恢复成原来的sys.stdout.write方法。下面例子解决这个问题:
- 创建一个变量,用于保存可能出现的错误消息。
- 处理ZeroDivisionError异常,设置一个错误消息。
- 撤销对sys.stdout.wirte方法做的猴子补丁。
- 如果设置了错误消息,就打印出来。