• 流畅的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 语句。

  • 相关阅读:
    如何设置java环境变量
    创建DLL动态链接库——模块定义法(def)
    创建DLL动态链接库——声明导出法
    fwrite()中参数含义——size和count经常用搞反
    解决VS2010中winsock.h与winsock2.h冲突(重复定义)——转载
    组播协议——IGMP v2报文头介绍
    IP/IGMP/UDP校验和算法
    POJ1625 Censored!
    HDU2222(Keywords Search,AC自动机)
    POJ1204 Word Puzzle(AC自动机)
  • 原文地址:https://www.cnblogs.com/pythonwl/p/15508268.html
Copyright © 2020-2023  润新知