《Fluent Python》 CH.15_控制流程_上下文管理器和else块 (不仅而且的else作用、@contextmanager实现上下文管理器协议、建议yield 语句用try-finally进行保护)

小结

  • 共计 61页

本章速读

  • with 语句会设置一个临时的上下文,交给上下文管理器对象控制,内部包含 __enter__ 和 __exit__ 两个方法。
  • if-else的要么要么结构,for/else、while/else 和 try/else属于不仅而且结构,大大地不同。
  • @contextmanager 装饰器优雅且实用,把三个不同的 Python 特性结合 到了一起:函数装饰器、生成器和 with 语句。

补充知识点

Python用户往往会忽视或没有充分使用这些特性。

下面要讨论的特性有:

  • with 语句和上下文管理器
  • for、while 和 try 语句的 else 子句

with 语句会设置一个临时的上下文,交给上下文管理器对象控制,并 且负责清理上下文。这么做能避免错误并减少样板代码,因此 API 更安全,而且更易于使用。除了自动关闭文件之外,with 块还有很多用途。

15.1 不仅而且, 不同于if-else的要么要么

for/else、while/else 和 try/else 的语义关系紧密,不过与 if/else 差别很大。

  • for-else 仅当 for 循环运行完毕时(即 for 循环没有被 break 语句中止) 才运行 else 块。
  • while-else 仅当 while 循环因为条件为假值而退出时(即 while 循环没有被 break 语句中止)才运行 else 块。
  • try-else 仅当 try 块中没有异常抛出时才运行 else 块。官方文档 (https://docs.python.org/3/reference/compound_stmts.html)还指 出:“else 子句抛出的异常不会由前面的 except 子句处理。”

在所有情况下,如果异常或者 return、break 或 continue 语句导致 控制权跳到了复合语句的主块之外,else 子句也会被跳过。

我觉得除了 if 语句之外,其他语句选择使用 else 关键字是 个错误。else 蕴含着“排他性”这层意思,例如“要么运行这个循 环,要么做那件事”。可是,在循环中,else 的语义恰好相
反:“运行这个循环,然后做那件事。”因此,使用 then 关键字更 好。then 在 try 语句的上下文中也说得通:“尝试运行这个,然后 做那件事。”可是,添加新关键字属于语言的重大变化,而 Guido 唯恐避之不及。 -- 作者
else == then的字面意思作用

在循环中使用 else 子句的方式如下述代码片段所示:

for item in [1,2,3]:
    if item == 'banana':
        break
    else:
        raise ValueError('no banana flover found!')


---------------------------------------------------------------------------

ValueError                                Traceback (most recent call last)

<ipython-input-1-346a526ea195> in <module>
      3         break
      4     else:
----> 5         raise ValueError('no banana flover found!')
      6 
      7 


ValueError: no banana flover found!

示例,except-else

复合语句的主块发生异常,跳过else

try:
    1/0
except OSError:
    print('OSError...')
else:
    raise RuntimeError('non OSError...')
---------------------------------------------------------------------------

ZeroDivisionError                         Traceback (most recent call last)

<ipython-input-2-bd4a5d8c2e8b> in <module>
      1 try:
----> 2     1/0
      3 except OSError: print('OSError...')
      4 else: raise RuntimeError('non OSError...')
      5 


ZeroDivisionError: division by zero

复合语句的主块未发生异常

try:
    1/1
except OSError:
    print('OSError...')
else:
    raise RuntimeError('non OSError...')

---------------------------------------------------------------------------

RuntimeError                              Traceback (most recent call last)

<ipython-input-3-99f2d3ada3c3> in <module>
      4     print('OSError...')
      5 else:
----> 6     raise RuntimeError('non OSError...')
      7 
      8 


RuntimeError: non OSError...

15.2 上下文管理器和with块

上下文管理器对象存在的目的是管理 with 语句,就像迭代器的存在是 为了管理 for 语句一样。 with 语句的目的是简化 try/finally 模式。

这种模式用于保证一段代 码运行完毕后执行某项操作,即便那段代码由于异常、return 语句或 sys.exit() 调用而中止,也会执行指定的操作。finally 子句中的代 码通常用于释放重要的资源,或者还原临时变更的状态。

上下文管理器协议包含 __enter__ 和 __exit__ 两个方法。with 语句 开始运行时,会在上下文管理器对象上调用 __enter__ 方法。with 语 句运行结束后,会在上下文管理器对象上调用 __exit__ 方法,以此扮 演 finally 子句的角色。

示例 15-1 with演示把文件对象当成上下文管理器使用

with open('mirror.py') as fp:
    src = fp.read(60)

---------------------------------------------------------------------------

FileNotFoundError                         Traceback (most recent call last)

<ipython-input-4-b31d34c23dc8> in <module>
----> 1 with open('mirror.py') as fp:
      2     src = fp.read(60)


FileNotFoundError: [Errno 2] No such file or directory: 'mirror.py'

上下文管理器协议

包含 enterexit 两个方法。with 语句 开始运行时,会在上下文管理器对象上调用 enter 方法。with 语 句运行结束后,会在上下文管理器对象上调用 exit 方法,以此扮演 finally 子句的角色。

与函数和模块不同,with 块没有定义新的作用域。

示例 15-3 是 LookingGlass 类的实现。

class LookingGlass:
    def __enter__(self):
        import sys
        self.original_write = sys.stdout.write
        sys.stdout.write = self.reverse_write
        return 'JABBERWOCKY'
    def reverse_write(self, text):
        self.original_write(text[::-1])
    def __exit__(self, exc_type, exc_value, traceback):
        import sys
        sys.stdout.write = self.original_write
        if exc_type is ZeroDivisionError:
            print('Please DO NOT divide by zero!')
            return True
        print('__exit__')

with LookingGlass() as what:
    print('hello')
    print('what')

what
olleh
tahw
__exit__





'JABBERWOCKY'

上下文管理器中的一些新的示例

  • 在 sqlite3 模块中用于管理事务
  • 在 threading 模块中用于维护锁、条件和信号
  • 为 Decimal 对象的算术运算设置环境,参见 decimal.localcontext 函数的文档
  • 为了测试临时给对象打补丁,参见 unittest.mock.patch 函数的 文档

15.3 contextlib模块中的上下文管理工具

closing

如果对象提供了 close() 方法,但没有实现 enter/exit 协议,那么可以使用这个函数构建上下文管理器。

suppress

构建临时忽略指定异常的上下文管理器。

@contextmanager

这个装饰器把简单的生成器函数变成上下文管理器,这样就不用创 建类去实现管理器协议了。

ContextDecorator

这是个基类,用于定义基于类的上下文管理器。这种上下文管理器 也能用于装饰函数,在受管理的上下文中运行整个函数。

ExitStack

这个上下文管理器能进入多个上下文管理器。with 块结束 时,ExitStack 按照后进先出的顺序调用栈中各个上下文管理器的 exit 方法。如果事先不知道 with 块要进入多少个上下文管理器,可以使用这个类。例如,同时打开任意一个文件列表中的所有文件。

15.4 使用@contextmanager

@contextmanager 装饰器能减少创建上下文管理器的样板代码量,因为不用编写一个完整的类,定义 enterexit 方法,而只需实现有一个 yield 语句的生成器,生成想让 enter 方法返回的 值。

示例 15-5 mirror_gen.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
    # 产出一个值,这个值会绑定到 with 语句中 as 子句的目标变量上。 执行 with 块中的代码时,这个函数会在这一点暂停。
    yield 'JABBERWOCKY'
    sys.stdout.write = original_write
with looking_glass() as what:
    print('Alice, Kitty and Snowdrop')
    print(what)
what
pordwonS dna yttiK ,ecilA
YKCOWREBBAJ





'JABBERWOCKY'

建议yield 语句用try-finally进行保护

如果在 with 块中抛出了异常,Python 解释器会将其捕获,然后在 looking_glass 函数的 yield 表达式里再次 抛出。

但是,那里没有处理错误的代码,因此 looking_glass 函数会 中止,永远无法恢复成原来的 sys.stdout.write 方法,导致系统处 于无效状态。

使用 @contextmanager 装饰器时,要把 yield 语句放在 try/finally 语句中(或者放在 with 语句中),这是无法避免 的,因为我们永远不知道上下文管理器的用户会在 with 块中做什 么。

示例 15-8 用于原地重写文件的上下文管理器

import csv
with inplace(csvfilename, 'r', newline='') as (infh, outfh):
    reader = csv.reader(infh)
    writer = csv.writer(outfh)
    for row in reader:
    row += ['new', 'columns']
    writer.writerow(row)

inplace 函数是个上下文管理器,为同一个文件提供了两个句柄(这个 示例中的 infh 和 outfh),以便同时读写同一个文件。这比标准库中 的 fileinput.input 函数 (https://docs.python.org/3/library/fileinput.html#fileinput.input;顺便说一 下,这个函数也提供了一个上下文管理器)易于使用。

posted @ 2021-03-01 18:39  山枫叶纷飞  阅读(93)  评论(0编辑  收藏  举报