十五、上下文管理器和 else 块
with 语句会设置一个临时的上下文,交给上下文管理器对象控制,并且负责清理上下文。
能避免错误并减少样板代码,因此API更安全,而且更易于使用。
else 子句与 with 语句完全没有关系。
if 语句之外的 else 块
先做这个,再做那个
for/else
仅当 for 循环运行完毕时(即 for 循环没有被 break 语句中止),才运行 else 块。
while/else
仅当 while 循环因为条件为假而退出时(即 while 循环没有被 break 语句中止),才运行 else 块。
try/else
仅当 try 块中没有异常抛出时才运行 else 块。else 子句抛出的异常不会由前面的 except 子句处理。
在所有情况下,如果异常或者 return、break、continue 语句导致控制权跳到了复合语句的主块之外,else 子句也会被跳过。
In [1]: for i in range(10):
...: if i == 5:
...: continue
...: print(i)
...: else:
...: print('else!!!')
...:
0
1
2
3
4
6
7
8
9
else!!!
In [2]: for i in range(10):
...: if i == 5:
...: break
...: print(i)
...: else:
...: print('else!!!')
...:
0
1
2
3
4
In [3]: try:
...: a = 1/0
...: except:
...: print('tryerror')
...: else:
...: print('else!!!')
...: b = 1/0
...:
tryerror
In [4]: try:
...: a = 1
...: except:
...: print('tryerror')
...: else:
...: print('else!!!')
...: b = 1/0
...:
else!!!
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
<ipython-input-4-b818a73eb4f6> in <module>
5 else:
6 print('else!!!')
----> 7 b = 1/0
8
ZeroDivisionError: division by zero
实际使用:
for
for item in my_list:
if item.flavor == 'banana':
break
else:
raise ValueError('No banana flavor found!')
try/else
1 你觉得没必要使用
try:
dangerous_call()
after_call()
except OSError:
log('OSError...')
2 但是,try 捕捉的只是 dangerous_call() 的错误,不包括 after_call() 的,而且只有 dangerous_call() 成功执行了之后,after_call() 才能,也才应该执行。
try:
dangerous_call()
except OSError:
log('OSError...')
else:
after_call()
在 python 中,try/except 不仅用于处理错误,还常用于控制流程。
python 官方词汇表:
EAFP: 取得原谅比获得许可容易(easier to ask for forgiveness than permission)。该编程风格:简单明快,特点:代码中有很多 try 和 except 语句。像 C 一样。
LBYL: 三思而后行(look before you leap)。该编程风格与 EAFP 对立。在调用函数或查找属性或键之前,显式测试前提条件。特点:代码中有很多 if 语句。在多线程环境中,LBYL 风格可能会在“检查”和“行事”的空当引入条件竞争。例如,对 if key in mapping: return mapping[key]
这段代码来说,如果在测试之后,但在查找之前,另一个线程从映射中删除了那个键,那么这段代码就会失败。这个问题可以使用锁或者 EAFP 风格解决。
上下文管理器和 with 块
上下文管理器对象存在的目的是管理 with 语句,就像迭代器的存在是为了管理 for 语句一样。
with 语句的目的是简化 try/finally 模式。这种模式用于保证一段代码执行完毕后执行某项操作,即便那段代码由于异常、return、sys.exit() 调用而终止,也会执行指定的操作。finally 子句中的代码通常用于释放重要的资源,或者还原临时变更的状态。
In [5]: def aaa():
...: try:
...: return
...: finally:
...: print('finally!!!')
...:
In [6]: aaa()
finally!!!
上下文管理器协议:__enter__
和 __exit__
两个方法。
with 语句开始运行时,调用:__enter__
方法。
with 语句运行结束后,调用:__exit__
方法。以此扮演 finally 子句的角色。
In [7]: p = r'C:\Users\WangLin\Desktop\version8\socket_fins_server_test.py'
In [11]: with open(p,'r',encoding='utf8') as fp: # __enter__ 方法返回 self
...: s = fp.read(60)
...:
In [12]: len(s)
Out[12]: 60
In [13]: s
Out[13]: "'''fins socket测试服务端'''\nimport socket\n\n# 握手命令\n# 46494e53 0000"
In [14]: fp # fp变量仍然可用,with 没有定义新的作用域
Out[14]: <_io.TextIOWrapper name='C:\\Users\\WangLin\\Desktop\\version8\\socket_fins_server_test.py' mode='r' encoding='utf8'>
In [15]: fp.closed
Out[15]: True
In [17]: fp.encoding
Out[17]: 'utf8'
In [18]: fp.read(60) # 不能执行 I/O 操作,因为 __exit__ 把文件关闭了
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-18-132011e948ad> in <module>
----> 1 fp.read(60)
ValueError: I/O operation on closed file.
执行 with 后面的表达式得到的结果是上下文管理器对象,不过,把值绑定到目标变量上(as 子句)是在上下文管理器对象上调用 __enter__
方法的结果。
open() 函数返回 TextIOWrapper 类的实例,而该实例的 __enter__
方法返回 self。不过,__enter__
方法除了返回上下文管理器之外,还可能返回其他对象。
不管控制流程以哪种方式退出 with 块,都会在上下文管理器对象上调用 __exit__
方法,而不是在 __enter__
方法返回的对象上调用。
with 语句的 as 子句是可选的。对 open 函数来说,必须加上 as 子句,以便获取文件的引用。不过,有些上下文管理器会返回 None,因为没什么有用的对象能提供给用户。
In [1]: class L:
...: def __enter__(self):
...: import sys
...: self.o_w = sys.stdout.write
...: sys.stdout.write = self.r_w # 猴子补丁,替换成自己的方法
...: return 'ABCDEFG'
...: def r_w(self, t):
...: self.o_w(t[::-1])
...: def __exit__(self,exc_type,exc_value,traceback): # 正常:参数:None,None,None
...: import sys # 重复导入模块不会消耗很多资源,因为 Python 会缓存导入的模块
...: sys.stdout.write = self.o_w # 还原成原来的 sys.stdout.write 方法
...: if exc_type is ZeroDivisionError:
...: print('除0???')
...: return True # 返回 True,告诉解释器,异常已经处理。
# 如果 __exit__ 方法,返回None,或者True之外的值,with 块中的任何异常都会向上冒泡。
...:
In [2]: l = L()
In [3]: l
Out[3]: <__main__.L at 0x221ef533358>
In [5]: l
Out[5]: 'ABCDEFG'
In [6]: print('go')
go
In [8]: with L() as l: # with 块中所有标准输出,都会反向。
...: print('go')
...: print(l)
...:
og
GFEDCBA
在实际使用中,如果应用程序接管了标准输出,可能会暂时把 sys.stdout 换成类似文件的其他对象,然后再切换成原来的版本。
exc_type
异常类(例如 ZeroDivisionError)。
exc_value
异常实例。有时会有参数传给异常构造方法,例如错误消息,这些参数可以使用 exc_value.args 获取。
traceback
traceback 对象。
在 try/finally 语句的 finally 块中调用 sys.exc_info()(https://docs.python.org/3/library/sys.html#sys.exc_info),得到的就是 __exit__
接收的这三个参数。鉴于 with 语句是为了取代大多数 try/finally 语句,而且通常需要调用 sys.exc_info() 来判断做什么清理操作,这种行为是合理的。
In [9]: l1 = L()
In [10]: l1
Out[10]: <__main__.L at 0x221ef411c18>
In [11]: l11 = l1.__enter__()
In [12]: l11
Out[12]: 'GFEDCBA'
In [13]: l11 == 'GFEDCBA'
Out[13]: eslaF
In [14]: l11
Out[14]: 'GFEDCBA'
In [15]: l11 == 'GFEDCBA'
Out[15]: eslaF
In [16]: print('abc')
cba
In [17]: ll.__exit__(None,None,None)
denifed ton si 'll' eman :m0[rorrEemaNm13;1[
m0[
m0[m0[m33;1[m0[m33;1[m0[)m33;1[m0[enoNm23;1[m0[,m33;1[m0[enoNm23;1[m0[,m33;1[m0[enoNm23;1[m0[(m33;1[m0[__tixe__m0[m0[.m33;1[m0[llm0[ m33;1[1 >----m23;1[
m0[m43;1[>eludom<m63;0[ ni m0[>2f1042b52c64-71-tupni-nohtypi<m23;1[
)tsal llac tnecer tsom( kcabecarT m0[rorrEemaNm13;1[
m0[---------------------------------------------------------------------------m13;1[
In [18]: print('abc')
cba
In [19]: ll.__exit__(None,None,None)
denifed ton si 'll' eman :m0[rorrEemaNm13;1[
m0[
m0[m0[m33;1[m0[m33;1[m0[)m33;1[m0[enoNm23;1[m0[,m33;1[m0[enoNm23;1[m0[,m33;1[m0[enoNm23;1[m0[(m33;1[m0[__tixe__m0[m0[.m33;1[m0[llm0[ m33;1[1 >----m23;1[
m0[m43;1[>eludom<m63;0[ ni m0[>2f1042b52c64-91-tupni-nohtypi<m23;1[
)tsal llac tnecer tsom( kcabecarT m0[rorrEemaNm13;1[
m0[---------------------------------------------------------------------------m13;1[
In [20]: l1
Out[20]: >81c114fe122x0 ta L.__niam__<
In [21]: l1.__exit__(None,None,None)
In [22]: print('abc')
abc
contextlib 模块中的实用工具
@contextmanager 装饰器能减少创建上下文管理器的样板代码量,因为不用编写一个完整的类,定义 __enter__
和 __exit__
方法,而只需实现有一个 yield 语句的生成器,生成想让 __enter__
方法返回的值。
在使用 @contextmanager 装饰的生成器中,yield 语句的作用是把函数的定义体分成两部分:yield 语句前面的所有代码在 with 块开始时(即解释器调用 __enter__
方法时)执行, yield 语句后面的代码在 with 块结束时(即调用 __exit__
方法时)执行。
In [23]: import contextlib
In [24]: @contextlib.contextmanager
...: def l():
...: import sys
...: o_w = sys.stdout.write
...: def r_w(t): # 闭包
...: o_w(t[::-1])
...: sys.stdout.write = r_w
...: yield 'ABCDE' # 返回上下文管理器对象
...: sys.stdout.write = o_w # 控制权一旦跳出 with 块,继续执行 yield 语句之后的代码;这里是恢复成原来的 sys. stdout.write 方法。
In [25]: with l() as w:
...: print('ABC')
...: print(w)
...:
CBA
EDCBA
In [26]: print('abc')
其实,contextlib.contextmanager 装饰器会把函数包装成实现 __enter__
和 __exit__
方法的类。
类的名称是 _GeneratorContextManager。如果想了解具体的工作方式,可以阅读 Python 3.4 发行版中Lib/contextlib.py 文件里的源码(https://hg.python.org/cpython/file/3.4/Lib/contextlib.py#l34)。
这个类的 __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 语句之后的代码。
In [27]: ll = l()
In [28]: print('abc')
abc
In [29]: next(ll)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-29-f4a50bb27fab> in <module>
----> 1 next(ll)
TypeError: '_GeneratorContextManager' object is not an iterator
In [30]: ll
Out[30]: <contextlib._GeneratorContextManager at 0x221ef42da90>
上面的上下文管理器有一个严重的错误:如果with块中抛出了异常,python解释器会将其捕获,然后再函数的yield 表达式里再次抛出。但是,没有异常处理。因此函数会中止,永远无法恢复成原来的 sys.stdout.write 方法,导致系统处于无效状态。
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: # 捕获 yield 异常
msg = 'Please DO NOT divide by zero!'
finally:
sys.stdout.write = original_write # 撤销猴子补丁
if msg:
print(msg)
前面说过,为了告诉解释器异常已经处理了,__exit__
方法会返回 True,此时解释器会压制异常。如果 __exit__
方法没有显式返回一个值,那么解释器得到的是 None,然后向上冒泡异常。使用 @contextmanager 装饰器时,默认的行为是相反的:装饰器提供的 __exit__
方法假定发给生成器的所有异常都得到处理了,因此应该压制异常。如果不想让 @contextmanager 压制异常,必须在被装饰的函数中显式重新抛出异常。
这样约定的原因是,创建上下文管理器时,生成器无法返回值,只能产出值。不过,现在可以返回值了,如 16.6 节所述。届时你会看到,如果在生成器中返回值,那么会抛出异常。
使用 @contextmanager 装饰器时,要把 yield 语句放在 try/finally 语句中(或者放在 with 语句中),这是无法避免的,因为我们永远不知道上下文管理器的用户会在 with 块中做什么。
In [49]: @contextlib.contextmanager
...: def io_f(f_n,mod,newline=''):
...: f_r = open(f_n,'rb')
...: import os
...: w_path = os.path.join(os.path.dirname(f_n),'copy' + os.path.basename(f_n))
...: f_w = open(w_path,'wb')
...: try:
...: yield f_r,f_w
...: except:
...: print('执行报错')
...: finally:
...: f_r.close()
...: f_w.close()
...:
In [50]:
In [50]: with io_f(r'C:\Users\WangLin\Desktop\version8\socket_fins_server_test.py','r') as (f_r,f_w):
...: f_w.write(f_r.read())
...:
用于原地重写文件的上下文管理器
inplace 函数是个上下文管理器,为同一个文件提供了两个句柄(这个示例中的 infh 和 outfh),以便同时读写同一个文件。这比标准库中的 fileinput.input 函数(https://docs.python.org/3/library/fileinput.html#fileinput.input;顺便说一下,这个函数也提供了一个上下文管理器)易于使用。
如果想学习 Martijn 实现 inplace 的源码(列在这篇文章中:http://www.zopatista.com/python/2013/11/26/inplace-file-rewriting/),找到 yield 关键字,在此之前的所有代码都用于设置上下文:先创建备份文件,然后打开并产出 __enter__
方法返回的可读和可写文件句柄的引用。yield 关键字之后的 __exit__
处理过程把文件句柄关闭;如果什么地方出错了,那么从备份中恢复文件。
@contextmanager 装饰器优雅且实用,把三个不同的 Python 特性结合到了一起:函数装饰器、生成器和 with 语句。