6-4-异常的设计

1. 嵌套异常处理器

try 语句可以嵌套,当 try/except 语句发生异常时,Python 会回到最近进入、具有相符 except 分句的 try 语句。一旦异常被捕捉,其生命就结束,控制权不会跳回所有匹配这个异常、相符的 try 语句;只有第一个 try 有机会对它进行处理。

当 try/finally 语句嵌套且发生异常时,每个 finally 代码块都会执行,Python 会持续把异常往上传递到其他 try 语句上,最终可能达到顶层默认处理器,除非有个 try/except 在这个过程中捕捉某处该异常。

1.1 例子:控制流程嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
def action2():
print(1 + []) # 产生 TypeError

def action1():
try:
action2()
except TypeError: # 最近匹配的 try
print('inner try')

try:
action1()
except TypeError:
print('outer try')
inner try

1.2 例子:语法嵌套化

1
2
3
4
5
6
7
try:
try:
action2()
except TypeError:
print('inner try')
except TypeError:
print('outer try')
inner try

嵌套处理器实际上是嵌入 try 代码块中,而不是写在其他被调用的函数中。

2. 异常的习惯用法

2.1 跳出多重嵌套循环:“go to”

异常经常被用来作为其他语言的“go to”语句来实现任意控制转移。raise 相当于“go to”,except 子句和异常名称作为程序标签。

1
2
3
4
5
6
7
8
9
10
11
12
class Exitloop(Exception): pass

try:
while True:
while True:
for i in range(10):
if i > 3: raise Exitloop
print('loop3: %s' % i)
print('loop2')
print('loop1')
except Exitloop:
print('continuing')
loop3: 0
loop3: 1
loop3: 2
loop3: 3
continuing

如果使用 break 替换 raise,则会进入无限循环,因为 break 只能跳出一层循环。

2.2 异常不总是错误

内置的 input 函数在每次调用时,在文件末尾时引发内置的 EOFError。
调用 sys.exit() 并在键盘上按下 Ctrl-C,会分别引发 SystemExit 和 KeyboardInterrupt。

2.3 函数通过 raise 表示状态

用户定义的异常也可引发非错误的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Found(Exception): pass

def searcher():
if ...success...:
raise Found()
else:
return

try:
searcher()
except Found:
...success...
else:
...failure...

2.4 关闭文件和服务器连接

2.5 在 try 外进行调试

1
2
3
4
5
try:
...run program...
except:
import sys
print('uncaught!', sys.exc_info()[0], sys.exc_info()[1])

在错误发生后,仍保持程序处于激活状态,这样可以执行其他测试,而不用重新开始。

2.6 运行进程中的测试

1
2
3
4
5
6
7
8
9
10
11
12
import sys
log = open('testlog', 'a')
from testapi import moreTests, runNextTest, testName
def testdriver():
while moreTests():
try:
runNextTest()
except:
print('FAILED', testName(), sys.exc_info()[:2], file=log)
else:
print('PASSED', testName(), file=log)
testdriver()

2.7 关于 sys.exc_info

通常允许一个异常处理器获取对最近引发的异常的访问。如果没有处理器正在处理,就返回包含了三个 None 值的元组。否则,将会返回(type、value 和 traceback):

  • type:正在处理的异常的异常类型
  • value:引发的异常类实例
  • traceback:代表异常最初发生时所调用的堆栈

sys.exc_info 主要由空的 except 使用。

2.8 显示错误和跟踪信息

1
2
3
4
5
6
7
8
9
10
import traceback

def inverse(x):
return 1 / x

try:
inverse(0)
except Exception:
traceback.print_exc(file=open('badly.exc', 'w'))
print('Bye')
Bye
1
!type badly.exc
Traceback (most recent call last):
  File "<ipython-input-6-b605ba4bc54e>", line 7, in <module>
    inverse(0)
  File "<ipython-input-6-b605ba4bc54e>", line 4, in inverse
    return 1 / x
ZeroDivisionError: division by zero

3. 异常设计技巧和陷阱

3.1 应该包装什么

  • 经常会失败的运算一般都应该包装在 try 语句内。例如,和系统状态衔接的运算(文件开启、套接字调用等)就是 try 的主要候选者。
  • 在简单的脚本中,你会希望这类运算失败时终止程序,而不是被捕捉或是被忽略。Python 中的错误会产生有用的出错消息,而且这通常就是所期望的最好结果。
  • 应该在 try/finally 中实现终止动作,从而保证它们的执行。
  • 偶尔,把对大型函数的调用包装在单个 try 语句内,而不是让函数本身零散着放入若干 try 语句中。

3.2 捕捉太多:避免空 except 语句

3.3 捕捉过少:使用基于类的分类

1
2
3
4
5
6
try:
pass
except (MyExcept1, MyExcept2):
pass
else:
pass

如果未来增加了 MyExcept3,就会视为错误并对其进行处理,除非更新异常列表。使用基于类的异常,可以让这种陷阱消失。如果捕捉一般的超类,就可以在未来新增和引发更为特定的子类,而不用手动扩展 except 分句的列表:超类会变成可扩展的异常分类。

异常处理器不要过于一般化,也不要过于太具体化,而且要明智选择 try 语句所包装的代码量。