第十五章 上下文管理器和else块
这是补的一块,前面看的时候,觉的用到的机会可能不多,就没写。
15.1讲了for,while,try结合else 的用法,这一块我已经掌握了,就不重复了。
书中的原句,在所有的情况下,如果异常或者return、break、或contiune语句导致控制权跳到了复合语句的主板之外,else字句也会被跳过。
书中最后介绍了一个有意思的玩意。
在Python中,try/except不仅用于处理错误,还常用于控制流畅。为此,Python官方词汇表还定义了一个缩略词(口号)。
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]这段代码来说,如果在测试之后,但在查找之前,另一个线程从映射中删除了那个键,那么这段代码就会失败。这个问题可使用锁或者EAFP风格解决。
15.2 上下文管理器和with块
with语句的目的是简化try/finally模式。这种模式用于保证一段代码运行完毕后执行某项操作,即便那段代码由于异常、return语句或sys.exit()调用而中支,也会执行指定的操作。finally子句中的代码通常用于释放重要的资源,或者还原临时变更的状态。
In [168]: with open("exec_new3.sh") as fp: ...: src = fp.read(20) ...: print(fp) ...: ...: <_io.TextIOWrapper name='exec_new3.sh' mode='r' encoding='UTF-8'> In [169]: len(src) Out[169]: 20 In [170]: fp Out[170]: <_io.TextIOWrapper name='exec_new3.sh' mode='r' encoding='UTF-8'> In [171]: fp.closed,fp.encoding Out[171]: (True, 'UTF-8') In [172]: fp.read(10) --------------------------------------------------------------------------- ValueError Traceback (most recent call last) <ipython-input-172-f949b87ba52f> in <module> ----> 1 fp.read(10) ValueError: I/O operation on closed file.
with与函数和模块不同,with块没有定义新的作用域
In [173]: class LookingGlass: ...: ...: def __enter__(self): ...: import sys ...: self.original_write = sys.stdout.write ...: sys.stdout.write = self.reveres_write ...: return 'JABBERWOCKY' ...: ...: def reveres_write(self, text): ...: return self.original_write(text[::-1]) ...: ...: def __exit__(self, exc_type, exc_val, exc_tb): ...: import sys ...: sys.stdout.write = self.original_write ...: if exc_type is ZeroDivisionError: ...: print("Please DO NOT divide by zero!") ...: return True ...: ...: In [174]: with LookingGlass() as what: ...: print("Alice. kitty and Snowdrop") ...: print(what) ...: pordwonS dna yttik .ecilA YKCOWREBBAJ In [175]: what Out[175]: 'JABBERWOCKY' In [176]: print('hello') hello In [177]: In [177]: with LookingGlass() as what: ...: print("Alice. kitty and Snowdrop") ...: print(what) ...: 1/0 ...: ...: pordwonS dna yttik .ecilA YKCOWREBBAJ Please DO NOT divide by zero! In [178]: class LookingGlass: ...: ...: def __enter__(self): ...: import sys ...: self.original_write = sys.stdout.write ...: sys.stdout.write = self.reveres_write ...: return 'JABBERWOCKY' ...: ...: def reveres_write(self, text): ...: return self.original_write(text[::-1]) ...: ...: def __exit__(self, exc_type, exc_val, exc_tb): ...: import sys ...: sys.stdout.write = self.original_write ...: if exc_type is ZeroDivisionError: ...: print("Please DO NOT divide by zero!") ...: ...: ...: In [179]: with LookingGlass() as what: ...: print("Alice. kitty and Snowdrop") ...: print(what) ...: 1/0 ...: ...: ...: pordwonS dna yttik .ecilA YKCOWREBBAJ Please DO NOT divide by zero! --------------------------------------------------------------------------- ZeroDivisionError Traceback (most recent call last) <ipython-input-179-74f77dac452b> in <module> 2 print("Alice. kitty and Snowdrop") 3 print(what) ----> 4 1/0 5 6 ZeroDivisionError: division by zero
这是书中的示范代码,在with的中通过猴子补丁修改sys.stdout的wirte方法。
这种我测试了最后一个重点,
如果__exit__方法返回None,或者True之外的值,with块中的任何异常都会向上冒泡。
In [2]: manager = LookingGlass() In [3]: manager Out[3]: <__main__.LookingGlass at 0x7fe17dfecfd0> In [4]: monster = manager.__enter__() In [5]: monster == "JABBERWOCKY" Out[5]: eurT In [6]: monster Out[6]: 'YKCOWREBBAJ' In [7]: manager Out[7]: >0dfcefd71ef7x0 ta ssalGgnikooL.__niam__< In [9]: manager.__exit__(* [None] * 3) In [10]: monster Out[10]: 'JABBERWOCKY' In [11]: manager Out[11]: <__main__.LookingGlass at 0x7fe17dfecfd0>
上面通过直接调用__enter__的方式运行,当调用__enter__就处于了该上下文环境中,通过__exit__退出设置的上下文环境。
15.4 使用@contextmanager
@contextmanager装饰器能减少创建上下文管理器的样板代码量,因为不用写一个完整的类,定义__enter__和__exit__方法,而只需实现有一个yield语句的生成器,生成向让__erter__方法返回的值。
在使用@contextmanager装饰的生成器中,yield语句的作用是把函数的定义体分成两部分;yield语句前面的所在代码在with块开始时(即解释器调用__enter__方法时)执行,yield语句后面的代码在with块结束时(既调用__exit__方法时)执行。
import contextlib @contextlib.contextmanager def looking_glass(): import sys original_write = sys.stdout.write def reverse_write(text): original_write(text[::-1]) sys.stdout.write = reverse_write yield 'JABBERWOCKY' # 这个就好比__enter__, 下面的就是__exit__ sys.stdout.write = original_write
经过测试,这个在__exit__没有做处理,如果在with的条件下出错,那状态就一直维护在__enter__下的状态。
所以需要对这个函数进行改进
import contextlib @contextlib.contextmanager def looking_glass(): import sys original_write = sys.stdout.write def reverse_write(text): original_write(text[::-1]) sys.stdout.write = reverse_write msg = '' try: yield 'JABBERWOCKY' # 这个就好比__enter__, 下面的就是__exit__ except ZeroDivisionError: msg = 'Please DO NOT divide by zero!' finally: sys.stdout.write = original_write if msg: print(msg)
这个是改进后的函数。
前面介绍的__exit__方法要返回True,此时解释器会压制异常。如果__exit__方法没有显式返回一个值,那么解释器得到的是None,然后向上冒泡异常。
使用@contextmanager装饰器时,默认的行为是相反的:如果压制了异常,不需要返回,如果不想压制异常,需要显式的抛出异常。
简单写一下,其实一般自己写上下文管理器我觉的机会不多。