Skip to content

3.3 异常 ​

INFO

来源:3.3 Exceptions

对应:无

程序员必须时刻留意程序中可能出现的错误。这样的例子很多:函数可能收不到它本来设计要接受的参数,必要的资源可能缺失,或者网络连接可能中断。设计程序时,必须预见到可能出现的异常情况,并采取适当措施来处理。

处理程序中的错误,没有唯一正确的方法。有些程序要提供持续的服务,比如网络服务器,这类程序应当对错误保持健壮:把错误记录下来,留待日后查看,同时尽可能继续处理新的请求。另一方面,Python 解释器处理错误的方式是立即终止程序并打印错误消息,好让程序员在问题一出现时就着手解决。无论哪种情况,程序员都必须有意识地选择程序该如何应对异常情况。

异常(Exceptions)是本节的主题。异常提供了一种通用机制,能往程序里加入处理错误的逻辑。抛出异常(raising an exception)是一种技术,用来中断程序的正常执行流程,表明出现了某种异常情况,并直接返回到程序中预先指定好、用来应对这种情况的那部分。Python 解释器每次检测到表达式或语句里有错误,都会抛出异常。用户也可以用 raise 和 assert 语句抛出异常。

抛出异常。异常是一个对象实例,它的类(class)直接或间接继承自 BaseException。第 1 章引入的 assert 语句会抛出一个类为 AssertionError 的异常。一般来说,任何异常实例都可以用 raise 语句抛出。raise 语句的一般形式见 Python 文档。raise 最常见的用法,是构造一个异常实例并把它抛出。

python
>>> 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 开头:

python
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 绑定到 0。

python
>>> try:
		x = 1/0
	except ZeroDivisionError as e:
		print('handling a', type(e))
		x = 0
handling a <class 'ZeroDivisionError'>
>>> x
0

try 语句会处理发生在 <try suite> 内某个函数体里的异常,只要这个函数是在 <try suite> 里被施加的,无论直接还是间接。抛出异常时,控制权会直接跳到最近一个能处理该类型异常的 try 语句的 <except suite> 里。

python
>>> 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 的新类。

python
>>> class IterImproveError(Exception):
        def __init__(self, last_guess):
            self.last_guess = last_guess

接下来,我们定义 improve 的一个版本,也就是我们通用的迭代改进算法。这个版本会抛出 IterImproveError 来处理任何 ValueError,并把最近的一次猜测存进去。和之前一样,improve 接受两个函数作为参数,每个函数都接受一个数值参数。update 函数返回新的猜测,done 函数返回一个布尔值,表示迭代改进是否已收敛到一个正确的值。

python
>>> 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 时会返回最后的猜测。

python
>>> 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 施加到函数 2x2+x 上,求它的零点。这个函数在 0 处有一个零点,但把任何负数代进去求值都会抛出 ValueError。我们在第 1 章实现的牛顿法会抛出这个错误,无法返回关于零点的任何猜测。而我们修订后的实现会返回错误发生前找到的最后一个猜测。

python
>>> from math import sqrt
>>> find_zero(lambda x: 2*x*x + sqrt(x))
-0.030211203830201594

尽管这个近似值离正确答案 0 还很远,但有些应用宁愿要这个粗略的近似值,也不愿碰到 ValueError。

异常是另一种技术,帮我们把程序的各种关注点拆成相互独立的模块。在这个例子里,Python 的异常机制让我们能把迭代改进的逻辑(它在 try 子句的 suite 里保持不变)和处理错误的逻辑(它出现在 except 子句里)分开。我们还会看到,在用 Python 实现解释器时,异常也是一个有用的特性。

基于 MIT 许可发布