深入学习Python中的上下文管理器与else块
前言
本文主要个大家介绍了关于Python上下文管理器与else块的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧。
在开始之前,我们先来看看下面这段话:
最终,上下文管理器可能几乎与子程序(subroutine)本身一样重要。目前,我们只了解了上下文管理器的皮毛……Basic 语言有with 语句,而且很多语言都有。但是,在各种语言中 with 语句的作用不同,而且做的都是简单的事,虽然可以避免不断使用点号查找属性,但是不会做事前准备和事后清理。不要觉得名字一样,就意味着作用也一样。with 语句是非常了不起的特性。
——Raymond Hettinger
雄辩的 Python 布道者
先做这个,再做那个:if语句之外的else块
这个语言特性不是什么秘密,但却没有得到重视:else 子句不仅能在if 语句中使用,还能在 for、while 和 try 语句中使用。for/else、while/else 和 try/else 的语义关系紧密,不过与if/else 差别很大。起初,else 这个单词的意思阻碍了我对这些特性的理解,但是最终我习惯了。
else 子句的行为如下:
for
仅当 for 循环运行完毕时(即 for 循环没有被 break 语句中止)才运行 else 块。
while
仅当 while 循环因为条件为假值而退出时(即 while 循环没有被break 语句中止)才运行 else 块。
try
仅当 try 块中没有异常抛出时才运行 else 块。官方文档(https://docs.python.org/3/reference/compound_stmts.html)还指出:“else 子句抛出的异常不会由前面的 except 子句处理。”
注意:
在所有情况下,如果异常或者 return、break 或 continue 语句导致控制权跳到了复合语句的主块之外,else 子句也会被跳过。
在这些语句中使用 else 子句通常能让代码更易于阅读,而且能省去一些麻烦,不用设置控制标志或者添加额外的 if 语句。
在循环中使用 else 子句的方式如下述代码片段所示:
for item in my_list: if item.flavor == 'banana': break else: raise ValueError('No banana flavor found!')
一开始,你可能觉得没必要在 try/except 块中使用 else 子句。毕竟,在下述代码片段中,只有 dangerous_call()
不抛出异常,after_call()
才会执行,对吧?
try: dangerous_call() after_call() except OSError: log('OSError...')
然而,after_call()
不应该放在 try 块中。为了清晰和准确,try 块中应该只抛出预期异常的语句。因此,像下面这样写更好:
try: dangerous_call() except OSError: log('OSError...') else: after_call()
现在很明确,try 块防守的是 dangerous_call()
可能出现的错误,而不是 after_call()
。而且很明显,只有 try 块不抛出异常,才会执行after_call()
。
上下文管理器和with块
上下文管理器对象存在的目的是管理 with 语句,就像迭代器的存在是为了管理 for 语句一样。
with 语句的目的是简化 try/finally 模式。这种模式用于保证一段代码运行完毕后执行某项操作,即便那段代码由于异常、return 语句或sys.exit()
调用而中止,也会执行指定的操作。finally 子句中的代码通常用于释放重要的资源,或者还原临时变更的状态。
上下文管理器协议包含 __enter__ 和 __exit__ 两个方法。with 语句开始运行时,会在上下文管理器对象上调用 __enter__ 方法。with 语句运行结束后,会在上下文管理器对象上调用 __exit__ 方法,以此扮演 finally 子句的角色。
🌰 演示把文件对象当成上下文管理器使用
>>> with open('mirror.py') as fp: # fp绑定到打开的文件上,因为文件的__enter__方法返回self ... src = fp.read(60) # 从fp中读取一些数据 ... >>> len(src) >>> fp # fp变量依然可以使用 <_io.TextIOWrapper name='mirror.py' mode='r' encoding='UTF-8'> >>> fp.closed, fp.encoding # 可以读取fp对象的属性 (True, 'UTF-8') >>> fp.read(60) # 但是不能在fp上执行I/O操作,因为在with块的结尾,调用了TextIOWrappper.__exit__方法把文件关闭了 Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: I/O operation on closed file.
测试 LookingGlass 上下文管理器类
>>> from mirror import LookingGlass >>> with LookingGlass() as what: # 上下文管理器是LookingGlass类的实例;Python在上下文管理器上调用__enter__方法,把返回结果绑定在what上 ... print('Alice, Kitty and Snowdrop') # 打印一个字符串,然后打印what变量的值 ... print(what) ... pordwonS dna yttiK ,ecilA # 打印出的内容是反向的 YKCOWREBBAJ >>> what # 现在,with块已经执行完毕,可以看出,__enter__方法返回的值,即存储在what变量中的值是字符串'JABBERWOCKY' 'JABBERWOCKY' >>> print('Back to normal.') # 输出不在是反向的了 Back to normal.
mirror.py:LookingGlass 上下文管理器类的代码
class LookingGlass: def __enter__(self): # 除了 self 之外,Python调用__enter__方法时不窜入其他参数 import sys self.original_write = sys.stdout.write # 把原来的 sys.stdout.write 方法保存在一个实例属性中,供后面使用 sys.stdout.write = self.reverse_write # 为 sys.stdout.write 打猴子补丁,替换成自己编写的方法 return 'JABBERWOCKY' # 返回 'JABBERWOCKY' 字符串,这样才有内容存入目标变量 what def reverse_write(self, text): # 这是用于取代 sys.stdout.write 的方法,把 text 参数的内容反转,然后调用原来的方法实现 return self.original_write(text[::-1]) def __exit__(self, exc_type, exc_val, traceback): # 如果一切正常,Python会调用__exit__方法传入的参数是三个None,如果抛出异常,则三个参数是异常的数据 import sys sys.stdout.write = self.original_write # 还原成原来的sys.studout.write方法 if exc_type is ZeroDivisionError: # 如果有异常,而且是 ZeroDivisionError 类型,打印一个消息 print('Please DO NOT divide by zero!') return True # 然后返回 True,告诉解释器,异常已经处理
解释器调用 __enter__ 方法时,除了隐式的 self 之外,不会传入任何参数。传给 __exit__ 方法的三个参数列举如下。
exc_type
异常类(例如 ZeroDivisionError)
exc_value
异常实例。有时会有参数传给异常构造方法,例如错误消息,这些参数可以使用 exc_value.args 获取
traceback
traceback 对象
在 with 块之外使用 LookingGlass 类
>>> from mirror import LookingGlass >>> manager = LookingGlass() # 实例化并审查manager实例,等同于 with LookingGlass() as manager >>> manager <mirror.LookingGlass object at 0x2a578ac> >>> monster = manager.__enter__() # 在上下文管理器中调用__enter__()方法,把结果存储在monster中 >>> monster == 'JABBERWOCKY' # monster的值是字符串'JABBERWOCKY',打印出来的True标识符是反向,因为用了猴子补丁 eurT >>> monster 'YKCOWREBBAJ' >>> manager >ca875a2x0 ta tcejbo ssalGgnikooL.rorrim< >>> manager.__exit__(None, None, None) # 调用manager.__exit__,还原成之前的stdout.write >>> monster 'JABBERWOCKY'
contextlib模块中的实用工具
closing
如果对象提供了 close()
方法,但没有实现__enter__/__exit__ 协议,那么可以使用这个函数构建上下文管理器。
suppress
构建临时忽略指定异常的上下文管理器。
@contextmanager
这个装饰器把简单的生成器函数变成上下文管理器,这样就不用创建类去实现管理器协议了。
ContextDecorator
这是个基类,用于定义基于类的上下文管理器。这种上下文管理器也能用于装饰函数,在受管理的上下文中运行整个函数。
ExitStack
这个上下文管理器能进入多个上下文管理器。with 块结束时,ExitStack 按照后进先出的顺序调用栈中各个上下文管理器的__exit__ 方法。如果事先不知道 with 块要进入多少个上下文管理器,可以使用这个类。例如,同时打开任意一个文件列表中的所有文件。
使用@contextmanager
@contextmanager 装饰器能减少创建上下文管理器的样板代码量,因为不用编写一个完整的类,定义 __enter__ 和 __exit__ 方法,而只需实现有一个 yield 语句的生成器,生成想让 __enter__ 方法返回的值。
在使用 @contextmanager 装饰的生成器中,yield 语句的作用是把函数的定义体分成两部分:yield 语句前面的所有代码在 with 块开始时(即解释器调用 __enter__ 方法时)执行, yield 语句后面的代码在with 块结束时(即调用 __exit__ 方法时)执行。
mirror_gen.py:使用生成器实现的上下文管理器
import contextlib @contextlib.contextmanager # 应用 contextmanager 装饰器 def looking_glass(): import sys original_write = sys.stdout.write # 贮存原来的 sys.stdout.write 方法 def reverse_write(text): # 定义自定义的 reverse_write 函数;在闭包中可以访问 original_write original_write(text[::-1]) sys.stdout.write = reverse_write # 把 sys.stdout.write 替换成 reverse_write yield 'JABBERWOCKY' # 产出一个值,这个值会绑定到 with 语句中 as 子句的目标变量上 sys.stdout.write = original_write # 控制权一旦跳出 with 块,继续执行 yield 语句之后的代码;这里是恢复成原来的 sys. stdout.write 方法 with looking_glass() as what: # 直接通过上下文管理器实现with的功能 print('Alice, Kitty and Snowdrop') print(what) print(what)
以上代码执行的结果为:
pordwonS dna yttiK ,ecilA YKCOWREBBAJ JABBERWOCKY
其实,contextlib.contextmanager 装饰器会把函数包装成实现__enter__ 和 __exit__ 方法的类
这个类的 __enter__ 方法有如下作用:
(1) 调用生成器函数,保存生成器对象(这里把它称为 gen)。
(2) 调用 next(gen),执行到 yield 关键字所在的位置。
(3) 返回 next(gen) 产出的值,以便把产出的值绑定到 with/as 语句中的目标变量上。
with 块终止时,__exit__ 方法会做以下几件事:
(1) 检查有没有把异常传给 exc_type;如果有,调用gen.throw(exception)
, 在生成器函数定义体中包含 yield 关键字的那一行抛出异常。
(2) 否则,调用 next(gen)
,继续执行生成器函数定义体中 yield 语句之后的代码。
注意:
上面的 🌰 有一个严重的错误:如果在 with 块中抛出了异常,Python 解释器会将其捕获,然后在 looking_glass 函数的 yield 表达式里再次抛出。但是,那里没有处理错误的代码,因此 looking_glass 函数会中止,永远无法恢复成原来的 sys.stdout.write
方法,导致系统处于无效状态。
mirror_gen_exc.py:基于生成器的上下文管理器,而且实现了异常处理
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' except ZeroDivisionError: #处理 ZeroDivisionError 异常,设置一个错误消息 msg = 'Please DO NOT divide by zero!' finally: sys.stdout.write = original_write # 撤销对 sys.stdout.write 方法所做的猴子补丁 if msg: print(msg) # 如果设置了错误消息,把它打印出来
注意:
使用 @contextmanager 装饰器时,要把 yield 语句放在try/finally 语句中(或者放在 with 语句中),这是无法避免的,因为我们永远不知道上下文管理器的用户会在 with 块中做什么。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对【听图阁-专注于Python设计】的支持。