# 3.3 异常

程序员必须时刻注意在他们的程序中可能出现的错误。 这样的例子有很多：一个函数可能不能接受它设计来接受的参数，一个必要的资源可能丢失，或者一个网络连接可能丢失。在设计程序时，必须预见可能出现的异常情况，并采取适当的措施来处理它们。&#x20;

在程序中没有一种正确的方法来处理错误。为提供持久服务(如web服务器)而设计的程序应该对错误具有健壮性，记录错误以供以后考虑，但要尽可能长时间地继续服务新请求。 另一方面，Python解释器通过立即终止并打印错误消息来处理错误，这样程序员就可以在出现问题时立即处理。 在任何情况下，程序员必须有意识地选择他们的程序应该如何对异常情况作出反应。&#x20;

异常是本节的主题，它提供了一种向程序中添加错误处理逻辑的通用机制。 引发异常是一种技术，用于中断程序中的正常执行流，表明出现了某些异常情况，并直接返回到程序中指定对该情况作出反应的封闭部分。 Python解释器每次检测到表达式或语句中的错误都会抛出一个异常。 用户还可以使用raise和assert语句引发异常。&#x20;

**抛出异常。**&#x5F02;常是一个对象实例，其类直接或间接地继承自BaseException类。 第一章中介绍的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循环，或者如果Python以file参数开始，则完全终止。 此外，解释器将打印一个堆栈回溯，这是一个结构化的文本块，描述了引发异常的执行分支中的活动函数调用的嵌套集合。在上面的例子中，文件名\<stdin> 指示异常是由用户在交互式会话中引发的，而不是由文件中的代码引发的。&#x20;

**处理异常。** 异常可以通过封装的try语句来处理。 try语句由多个子句组成；第一个从try开始，其余的从except开始：

```
try:
    <try suite>
except <exception class> as <name>:
    <except suite>
...
```

当try语句执行时，\<try suite>总是立即执行。只有在执行的过程中引发异常时，except子句的\<except suite>才会被执行。每个except子句指定要处理的异常的特定类。例如，如果是AssertionError，那么在\<try suite>执行期间引发的继承AssertionError的任何类实例将由以下\<except suite>处理。在\<except suite>中，标识符\<name>被绑定到被抛出的异常对象，但该绑定不会持久存在于\<except suite>之外。

例如，我们可以使用try语句处理ZeroDivisionError异常，该语句在引发异常时将名称x绑定到0。

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

try语句将处理在一个函数体中发生的异常，该函数体被应用于\<try套件中(直接或间接地)。 当引发异常时，control直接跳转到\<except suite> 处理该类型异常的最新try语句的。

```
>>> def invert(x):
        result = 1/x  # Raises a ZeroDivisionError if x is 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\_safe中的print表达式，而是将控制转移到invert\_safe中的except子句的套件中。将ZeroDivisionError e强制转换为字符串会得到由invert\_safe返回的人类可解释的字符串：除零'。

### 3.3.1 异常对象

异常对象本身可以具有属性，比如assert语句中声明的错误消息，以及关于在执行过程中何处引发异常的信息。 用户定义的异常类可以有额外的属性。&#x20;

在第一章中，我们用牛顿法求任意函数的零点。 下面的示例定义了一个异常类，每当ValueError发生时，它将返回在迭代改进过程中发现的最佳猜测。 当sqrt应用于负数时，会引发一个数学域错误(ValueError类型)。 这个异常是通过抛出一个IterImproveError来处理的，它将来自Newton方法的最新猜测存储为一个属性。&#x20;

首先，我们定义一个继承自Exception的新类。

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

接下来，我们定义了improve的一个版本，即我们的通用迭代改进算法。这个版本通过抛出一个IterImproveError来处理ValueError，它存储了最近的猜测。和前面一样，improve接受两个函数作为参数，每个函数接受一个数值参数。update函数返回新的猜测，而done函数返回一个布尔值，表明improvement已经收敛到正确的值。

```
>>> 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，它返回应用于由newton\_update返回的Newton更新函数的改进结果，该函数在第一章中定义，本例不需要修改。这个版本的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来求函数 $$2x^2 + \sqrt{x}$$ 的零点。这个函数在0处有一个0，但是对任何负数求值都会引发ValueError。我们的第一章牛顿法的实现会产生这个错误，并且不能返回任何对0的猜测。我们修改后的实现返回错误之前发现的最后一个猜测。

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

虽然这个近似值离正确答案0还很远，但是一些应用程序更喜欢这种粗略的近似值而不是ValueError。

异常是另一种帮助我们作为程序将程序的关注点分离为模块化部分的技术。在这个例子中，Python的异常机制允许我们将用于迭代改进的逻辑(在try子句中没有改变)与用于处理错误的逻辑(出现在except子句中)分离开来。我们还会发现，在Python中实现解释器时，异常也是一个有用的特性。

