目录

上下文管理器和 else 块

本章内容

  • for、while 和 try 语句的 else 子句
  • EAFP 与 LBYL
  • 上下文管理器协议
  • with 语句和上下文管理器
  • contextlib 模块

1. else 子句

作用:

  1. 能让代码更易于阅读
  2. 不用设置控制标志或者添加额外的 if 语句

控制逻辑:

  • for :仅当 for 循环运行完毕时(没有被 break 语句中止)才运行 else 块
  • while:仅当 while 循环因为条件为假值而退出时(没有被 break 语句中止)才运行 else 块
  • try:
    • 仅当 try 块中没有异常抛出时才运行 else 块
    • else 子句抛出的异常不会由前面的 except 子句处理
  • 如果异常、return、break 或 continue 导致控制权跳到了复合语句的主块之外,else 子句就会被跳过
1
2
3
4
5
6
7
try:
    dangerous_call()
    # after_call()   不应该放在这
except OSError:
    log('OSError...')
else:
    after_call()

示例分析:

  • try 块中应该只捕获预期的异常
  • 这里捕获的是 dangerous_call() 可能出现的错误,而不是 after_call()
  • 只有 try 块不抛出异常,才会执行 after_call()

2. EAFP 与 LBYL

Python 官方词汇表:https://docs.python.org/3/glossary.html#term-eafp

EAFP:

  • 取得原谅比获得许可容易(easier to ask for forgiveness than permission)。这是一 种常见的 Python 编程风格,先假定存在有效的键或属性,如果假定不成立,那 么捕获异常。这种风格简单明快,特点是代码中有很多 try 和 except 语句。与 其他很多语言一样(如 C 语言),这种风格的对立面是 LBYL 风格。

LBYL:

  • 三思而后行(look before you leap)。这种编程风格在调用函数或查找属性或键之 前显式测试前提条件。与 EAFP 风格相反,这种风格的特点是代码中有很多 if 语句。在多线程环境中, LBYL 风格可能会在“检查”和“行事”的空当引入条 件竞争。例如,对 if key in mapping: return mapping[key] 这段代码来说,如 果在测试之后,但在查找之前,另一个线程从映射中删除了那个键,那么这段代 码就会失败。这个问题可以使用锁或者 EAFP 风格解决

2. 上下文管理器和with

上下文管理器对象存在的目的是管理 with 语句,就像迭代器的存在是为了管理 for 语句一样

2.1 with 语句

  • 作用:
    1. 简化 try/finally 模式
    • 这种模式用于保证一段代码运行完毕后执行某项操作,
    • 即便那段代码由于异常、return 语句或 sys.exit() 调用而中止,也会执行指定的操作
    • finally 子句中的代码通常用于释放重要的资源,或者还原临时变更的状态
    1. 不仅能管理资源,还能用于去掉常规的设置和清理代码,或者在另一个过程前后执行的操作
  • 特性:与函数和模块不同, with 块没有定义新的作用域
  • 原理:
    • with 语句会设置一个临时的上下文,交给上下文管理器对象控制
    • 上下文管理器对象实现了上下文管理器协议,在 with 语句执行的前后执行特定操作

执行过程

  1. 执行 with 后面的表达式,得到上下文管理器对象
  2. 执行上下文管理器对象的__enter__方法,返回值被绑定到目标变量上(as 子句)
  3. 不管控制流程以哪种方式退出 with 块,都会在上下文管理器对象上调用 __exit__ 方法
  4. with 语句的 as 子句是可选的
1
2
>>> with open('mirror.py') as fp:
... src = fp.read(60)

示例解析

  • open() 函数返回的 TextIOWrapper 类实例是上下文管理器对象
  • 该实例的 __enter__ 方法返回 self 赋值给 as 子句中的变量 fp
  • 在 with 块的末尾,调用 TextIOWrapper.__exit__ 方法把文件关闭
  • __enter__ 方法除了返回上下文管理器之外,还可能返回其他对象

2.2 上下文管理器协议

协议接口

  1. __enter__(self)
  • 运行:with 语句开始运行时,在上下文管理器对象上调用
  • 参数:
  1. __exit__ (self, exc_type, exc_value, traceback)
  • 运行:with 语句运行结束后,在上下文管理器对象上调用,扮演着 finally 子句的角色
  • 异常:返回 None,或者 True 之外的值, with 块中的任何异常都会向上冒泡
  • 参数:
    • exc_type:异常类(例如 ZeroDivisionError)
    • exc_value 异常实例。有时会有参数传给异常构造方法,例如错误消息,这些参数可以使用 exc_value.args 获取
    • traceback:traceback 对象
  • 附注:
    • 如果一切正常, Python 调用 __exit__ 方法时传入的参数是 None, None, None
    • 在finally 块中调用 sys.exc_info(),得到的就是 __exit__ 接收的这三个参数

2.3 上下文管理器的应用

  1. 在 sqlite3 模块中用于管理事务
  1. 在 threading 模块中用于维护锁、条件和信号
  1. 为 Decimal 对象的算术运算设置环境
  1. 为了测试临时给对象打补丁

3. contextlib模块

2.1 实用工具

文档:https://docs.python.org/3/library/contextlib.html

|对象|说明| |: —|: —| |redirect_stdout|只需传入类似文件的对象,用于替代 sys.stdout| |closing|如果对象提供了 close() 方法,但没有实现 __enter__/__exit__ 协议,
那么可以使用这个函数构建上下文管理器| |suppress|构建临时忽略指定异常的上下文管理器| |ContextDecorator|这是个基类,用于定义基于类的上下文管理器
这种上下文管理器也能用于装饰函数,在受管理的上下文中运行整个函数| |ExitStack|这个上下文管理器能进入多个上下文管理器
with 块结束时,按照后进先出的顺序调用栈中各个上下文管理器的 __exit__ 方法
如果事先不知道 with 块要进入多少个上下文管理器,可以使用这个类
例如,同时打开任意一个文件列表中的所有文件|

2.2 @contextmanager

  • 作用:
    • 把简单的生成器函数变成上下文管理器,不用创建类去实现管理器协议
  • 方法:
    1. 只需实现有一个 yield 语句的生成器,生成想让__enter__ 方法返回的值
    2. yield 语句的作用是把函数的定义体分成两部分
    • yield 语句前面的所有代码在 with 块开始时(即解释器调用 __enter__ 方法时)执行
    • yield 语句后面的代码在 with 块结束时(即调用 __exit__ 方法时)执行
    1. 装饰器会把函数包装成实现 __enter__ 和 __exit__ 方法的类
    • 类的 __enter__ :
      • 调用生成器函数,保存生成器对象(这里把它称为 gen)。
      • 调用 next(gen),执行到 yield 关键字所在的位置。
      • 返回 next(gen) 产出的值,以便把产出的值绑定到 with/as 语句中的目标变量上
    • __exit__ 方法
      • 检查有没有把异常传给 exc_type;如果有,调用 gen.throw(exception), 在生成器函数定义体中包含 yield 关键字的那一行抛出异常。
      • 否则,调用 next(gen),继续执行生成器函数定义体中 yield 语句之后的代码
      • 因此使用 @contextmanager 装饰器时,要把 yield 语句放在 try/finally 语句中(或者放在 with 语句中)
  • 异常管理:
    • 使用 @contextmanager 装饰器时,异常的处理与上下文管理器协议是相反的
    • 装饰器提供的 __exit__ 方法假定发给生成器的所有异常都得到处理了,因此应该压制异常。
    • 如果不想让 @contextmanager 压制异常,必须在被装饰的函数中显式重新抛出异常
  • 附注:
    • 在 @contextmanager 装饰器装饰的生成器中, yield 与迭代没有任何关系
    • 生成器函数的作用更像是协程:执行到某一点时暂停,让客户代码运行,直到客户让协程继续做事
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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:
        msg = 'Please DO NOT divide by zero!'
    finally:
        sys.stdout.write = original_write
        if msg:
            print(msg)

>>> with looking_glass() as what:
... print('Alice, Kitty and Snowdrop')

➊ yield ‘JABBERWOCKY’

  • 这个值会绑定到 with 语句中 as 子句的目标变量上
  • 执行 with 块中的代码时,这个函数会在这一点暂停
  • 控制权跳出 with 块,继续执行 yield 语句之后的代码

2.3 原地文件重写

1
2
3
4
5
6
7
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),以便同时读写同一个文件

延伸阅读

Python:

Compound statements

上下文管理器的类型

上下文管里器

__enter__/__exit__

blog:

What Makes Python Awesome?

Is it a good practice to use try-except-else in Python

Transforming Code into Beautiful, Idiomatic Python

实用工具

The Python with Statement by Example

书籍:

《 Python 技术手册(第 2 版)》

  • 有一章是关于异常的,那一章极好地讨论了 EAFP 风格

《 Python Cookbook(第 3 版)中文版》

  • 8.3 让对象支持上下文管理协议
  • 9.22 以简单的方式定义上下文管理器

附注