流畅的python——15 上下文管理器和 else 块

十五、上下文管理器和 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 语句。

posted @ 2021-11-04 15:07  pythoner_wl  阅读(94)  评论(0编辑  收藏  举报