小结
- 共计 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'
上下文管理器协议
包含 enter 和 exit 两个方法。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 装饰器能减少创建上下文管理器的样板代码量,因为不用编写一个完整的类,定义 enter 和 exit 方法,而只需实现有一个 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;顺便说一 下,这个函数也提供了一个上下文管理器)易于使用。