3.3 异常
程序员必须时刻留意程序中可能出现的错误。这样的例子很多:函数可能收不到它本来设计要接受的参数,必要的资源可能缺失,或者网络连接可能中断。设计程序时,必须预见到可能出现的异常情况,并采取适当措施来处理。
处理程序中的错误,没有唯一正确的方法。有些程序要提供持续的服务,比如网络服务器,这类程序应当对错误保持健壮:把错误记录下来,留待日后查看,同时尽可能继续处理新的请求。另一方面,Python 解释器处理错误的方式是立即终止程序并打印错误消息,好让程序员在问题一出现时就着手解决。无论哪种情况,程序员都必须有意识地选择程序该如何应对异常情况。
异常(Exceptions)是本节的主题。异常提供了一种通用机制,能往程序里加入处理错误的逻辑。抛出异常(raising an exception)是一种技术,用来中断程序的正常执行流程,表明出现了某种异常情况,并直接返回到程序中预先指定好、用来应对这种情况的那部分。Python 解释器每次检测到表达式或语句里有错误,都会抛出异常。用户也可以用 raise 和 assert 语句抛出异常。
抛出异常。异常是一个对象实例,它的类(class)直接或间接继承自 BaseException。第 1 章引入的 assert 语句会抛出一个类为 AssertionError 的异常。一般来说,任何异常实例都可以用 raise 语句抛出。raise 语句的一般形式见 Python 文档。raise 最常见的用法,是构造一个异常实例并把它抛出。
>>> raise Exception('An error occurred')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Exception: an error occurred抛出异常时,当前代码块中后面的语句都不会再执行。除非异常被处理(见下文),否则解释器会直接返回到交互式的读取-求值-打印循环(read-eval-print loop);如果 Python 是用文件参数启动的,则会完全终止。此外,解释器还会打印一段堆栈回溯(stack backtrace):它是一段结构化的文本,描述了异常抛出时、执行分支中处于活动状态的嵌套函数调用。在上面的例子里,文件名 <stdin> 表明这个异常是用户在交互会话中抛出的,而不是来自文件里的代码。
处理异常(handling exceptions)。异常可以由外围的 try 语句来处理。try 语句由若干子句组成;第一个以 try 开头,其余的以 except 开头:
try:
<try suite>
except <exception class> as <name>:
<except suite>执行 try 语句时,<try suite> 总是立即执行。只有执行 <try suite> 的过程中抛出异常,才会执行各 except 子句的 suite。每个 except 子句指定它要处理的特定异常类。例如,如果 <exception class> 是 AssertionError,那么执行 <try suite> 过程中抛出的任何继承自 AssertionError 类的实例,都由随后的 <except suite> 处理。在 <except suite> 内部,标识符 <name> 绑定到被抛出的那个异常对象,但这个绑定不会延续到 <except suite> 之外。
例如,我们可以用一个 try 语句来处理 ZeroDivisionError,它在异常抛出时把名称 x 绑定到
>>> try:
x = 1/0
except ZeroDivisionError as e:
print('handling a', type(e))
x = 0
handling a <class 'ZeroDivisionError'>
>>> x
0try 语句会处理发生在 <try suite> 内某个函数体里的异常,只要这个函数是在 <try suite> 里被施加的,无论直接还是间接。抛出异常时,控制权会直接跳到最近一个能处理该类型异常的 try 语句的 <except suite> 里。
>>> def invert(x):
result = 1/x # 抛出一个异常(ZeroDivisionError) 如果 x 为 0
print('Never printed if x is 0')
return result
>>> def invert_safe(x):
try:
return invert(x)
except ZeroDivisionError as e:
return str(e)
>>> invert_safe(2)
Never printed if x is 0
0.5
>>> invert_safe(0)
'division by zero'这个例子说明,invert 里的 print 表达式永远不会被求值,控制权会转而进入 invert_safe 中 except 子句的 suite。把 ZeroDivisionError e 强制转换成字符串,就得到 invert_safe 返回的那个便于人阅读的字符串:「division by zero」。
3.3.1 异常对象
异常对象(Exception Object)本身可以有属性(attribute),比如 assert 语句里写的错误消息,以及异常在执行到哪一步被抛出的信息。用户自定义的异常类还可以有额外的属性。
第 1 章里,我们实现了牛顿法来求任意函数的零点。下面的例子定义了一个异常类:每当发生 ValueError,它就返回迭代改进过程中找到的最佳猜测。把 sqrt 施加到负数上,会抛出数学域错误(math domain error,ValueError 的一种)。处理这个异常的办法,是抛出一个 IterImproveError,把牛顿法里最近的一次猜测存为属性。
首先,定义一个继承自 Exception 的新类。
>>> class IterImproveError(Exception):
def __init__(self, last_guess):
self.last_guess = last_guess接下来,我们定义 improve 的一个版本,也就是我们通用的迭代改进算法。这个版本会抛出 IterImproveError 来处理任何 ValueError,并把最近的一次猜测存进去。和之前一样,improve 接受两个函数作为参数,每个函数都接受一个数值参数。update 函数返回新的猜测,done 函数返回一个布尔值,表示迭代改进是否已收敛到一个正确的值。
>>> def improve(update, done, guess=1, max_updates=1000):
k = 0
try:
while not done(guess) and k < max_updates:
guess = update(guess)
k = k + 1
return guess
except ValueError:
raise IterImproveError(guess)最后,定义 find_zero,它返回把 improve 施加到 newton_update 返回的牛顿更新函数上所得的结果。newton_update 在第 1 章定义,这个例子不需要对它做任何改动。这个版本的 find_zero 遇到 IterImproveError 时会返回最后的猜测。
>>> def find_zero(f, guess=1):
def done(x):
return f(x) == 0
try:
return improve(newton_update(f), done, guess)
except IterImproveError as e:
return e.last_guess考虑把 find_zero 施加到函数 ValueError。我们在第 1 章实现的牛顿法会抛出这个错误,无法返回关于零点的任何猜测。而我们修订后的实现会返回错误发生前找到的最后一个猜测。
>>> from math import sqrt
>>> find_zero(lambda x: 2*x*x + sqrt(x))
-0.030211203830201594尽管这个近似值离正确答案 ValueError。
异常是另一种技术,帮我们把程序的各种关注点拆成相互独立的模块。在这个例子里,Python 的异常机制让我们能把迭代改进的逻辑(它在 try 子句的 suite 里保持不变)和处理错误的逻辑(它出现在 except 子句里)分开。我们还会看到,在用 Python 实现解释器时,异常也是一个有用的特性。