<audio id="audio" title="08 | 异常处理：如何提高程序的稳定性？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0c/f7/0c5b7acad86cebfb547c88b1cd22cef7.mp3"></audio>

你好，我是景霄。

今天这节课，我想和你聊聊Python的异常处理。和其他语言一样，异常处理是Python中一种很常见，并且很重要的机制与代码规范。

我在实际工作中，见过很多次这样的情况：一位工程师提交了代码，不过代码某处忘记了异常处理。碰巧这种异常发生的频率不低，所以在代码push到线上后没多久，就会收到紧急通知——服务器崩溃了。

如果事情严重，对用户的影响也很大，这位工程师还得去专门的会议上做自我检讨，可以说是很惨了。这类事件层出不穷，也告诉我们，正确理解和处理程序中的异常尤为关键。

## **错误与异常**

首先要了解，Python中的错误和异常是什么？两者之间又有什么联系和区别呢？

通常来说，程序中的错误至少包括两种，一种是语法错误，另一种则是异常。

所谓语法错误，你应该很清楚，也就是你写的代码不符合编程规范，无法被识别与执行，比如下面这个例子：

```
if name is not None
    print(name)

```

If语句漏掉了冒号，不符合Python的语法规范，所以程序就会报错`invalid syntax`。

而异常则是指程序的语法正确，也可以被执行，但在执行过程中遇到了错误，抛出了异常，比如下面的3个例子：

```
10 / 0
Traceback (most recent call last):
  File &quot;&lt;stdin&gt;&quot;, line 1, in &lt;module&gt;
ZeroDivisionError: integer division or modulo by zero

order * 2
Traceback (most recent call last):
  File &quot;&lt;stdin&gt;&quot;, line 1, in &lt;module&gt;
NameError: name 'order' is not defined

1 + [1, 2]
Traceback (most recent call last):
  File &quot;&lt;stdin&gt;&quot;, line 1, in &lt;module&gt;
TypeError: unsupported operand type(s) for +: 'int' and 'list'

```

它们语法完全正确，但显然，我们不能做除法时让分母为0；也不能使用未定义的变量做运算；而让一个整型和一个列表相加也是不可取的。

于是，当程序运行到这些地方时，就抛出了异常，并且终止运行。例子中的`ZeroDivisionError` `NameError`和`TypeError`，就是三种常见的异常类型。

当然，Python中还有很多其他异常类型，比如`KeyError`是指字典中的键找不到；`FileNotFoundError`是指发送了读取文件的请求，但相应的文件不存在等等，我在此不一一赘述，你可以自行参考[相应文档](https://docs.python.org/3/library/exceptions.html#bltin-exceptions)。

## **如何处理异常**

刚刚讲到，如果执行到程序中某处抛出了异常，程序就会被终止并退出。你可能会问，那有没有什么办法可以不终止程序，让其照样运行下去呢？答案当然是肯定的，这也就是我们所说的异常处理，通常使用try和except来解决，比如：

```
try:
    s = input('please enter two numbers separated by comma: ')
    num1 = int(s.split(',')[0].strip())
    num2 = int(s.split(',')[1].strip())
    ... 
except ValueError as err:
    print('Value Error: {}'.format(err))

print('continue')
...

```

这里默认用户输入以逗号相隔的两个整形数字，将其提取后，做后续的操作（注意input函数会将输入转换为字符串类型）。如果我们输入`a,b`，程序便会抛出异常`invalid literal for int() with base 10: 'a'`，然后跳出try这个block。

由于程序抛出的异常类型是ValueError，和except block所catch的异常类型相匹配，所以except block便会被执行，最终输出`Value Error: invalid literal for int() with base 10: 'a'`，并打印出`continue`。

```
please enter two numbers separated by comma: a,b
Value Error: invalid literal for int() with base 10: 'a'
continue

```

我们知道，except block只接受与它相匹配的异常类型并执行，如果程序抛出的异常并不匹配，那么程序照样会终止并退出。

所以，还是刚刚这个例子，如果我们只输入`1`，程序抛出的异常就是`IndexError: list index out of range`，与ValueError不匹配，那么except block就不会被执行，程序便会终止并退出（continue不会被打印）。

```
please enter two numbers separated by comma: 1
IndexError Traceback (most recent call last)
IndexError: list index out of range

```

不过，很显然，这样强调一种类型的写法有很大的局限性。那么，该怎么解决这个问题呢？

其中一种解决方案，是在except block中加入多种异常的类型，比如下面这样的写法：

```
try:
    s = input('please enter two numbers separated by comma: ')
    num1 = int(s.split(',')[0].strip())
    num2 = int(s.split(',')[1].strip())
    ...
except (ValueError, IndexError) as err:
    print('Error: {}'.format(err))
    
print('continue')
...

```

或者第二种写法：

```
try:
    s = input('please enter two numbers separated by comma: ')
    num1 = int(s.split(',')[0].strip())
    num2 = int(s.split(',')[1].strip())
    ...
except ValueError as err:
    print('Value Error: {}'.format(err))
except IndexError as err:
    print('Index Error: {}'.format(err))

print('continue')
...

```

这样，每次程序执行时，except block中只要有一个exception类型与实际匹配即可。

不过，很多时候，我们很难保证程序覆盖所有的异常类型，所以，更通常的做法，是在最后一个except block，声明其处理的异常类型是Exception。Exception是其他所有非系统异常的基类，能够匹配任意非系统异常。那么这段代码就可以写成下面这样：

```
try:
    s = input('please enter two numbers separated by comma: ')
    num1 = int(s.split(',')[0].strip())
    num2 = int(s.split(',')[1].strip())
    ...
except ValueError as err:
    print('Value Error: {}'.format(err))
except IndexError as err:
    print('Index Error: {}'.format(err))
except Exception as err:
    print('Other error: {}'.format(err))

print('continue')
...

```

或者，你也可以在except后面省略异常类型，这表示与任意异常相匹配（包括系统异常等）：

```
try:
    s = input('please enter two numbers separated by comma: ')
    num1 = int(s.split(',')[0].strip())
    num2 = int(s.split(',')[1].strip())
    ...
except ValueError as err:
    print('Value Error: {}'.format(err))
except IndexError as err:
    print('Index Error: {}'.format(err))
except:
    print('Other error')

print('continue')
...

```

需要注意，当程序中存在多个except block时，最多只有一个except block会被执行。换句话说，如果多个except声明的异常类型都与实际相匹配，那么只有最前面的except block会被执行，其他则被忽略。

异常处理中，还有一个很常见的用法是finally，经常和try、except放在一起来用。无论发生什么情况，finally block中的语句都会被执行，哪怕前面的try和excep block中使用了return语句。

一个常见的应用场景，便是文件的读取：

```
import sys
try:
    f = open('file.txt', 'r')
    .... # some data processing
except OSError as err:
    print('OS error: {}'.format(err))
except:
    print('Unexpected error:', sys.exc_info()[0])
finally:
    f.close()

```

这段代码中，try block尝试读取file.txt这个文件，并对其中的数据进行一系列的处理，到最后，无论是读取成功还是读取失败，程序都会执行finally中的语句——关闭这个文件流，确保文件的完整性。因此，在finally中，我们通常会放一些**无论如何都要执行**的语句。

值得一提的是，对于文件的读取，我们也常常使用with open，你也许在前面的例子中已经看到过，with open会在最后自动关闭文件，让语句更加简洁。

## **用户自定义异常**

前面的例子里充斥了很多Python内置的异常类型，你可能会问，我可以创建自己的异常类型吗？

答案是肯定是，Python当然允许我们这么做。下面这个例子，我们创建了自定义的异常类型MyInputError，定义并实现了初始化函数和str函数（直接print时调用）：

```
class MyInputError(Exception):
    &quot;&quot;&quot;Exception raised when there're errors in input&quot;&quot;&quot;
    def __init__(self, value): # 自定义异常类型的初始化
        self.value = value
    def __str__(self): # 自定义异常类型的string表达形式
        return (&quot;{} is invalid input&quot;.format(repr(self.value)))
    
try:
    raise MyInputError(1) # 抛出MyInputError这个异常
except MyInputError as err:
    print('error: {}'.format(err))

```

如果你执行上述代码块并输出，便会得到下面的结果：

```
error: 1 is invalid input

```

实际工作中，如果内置的异常类型无法满足我们的需求，或者为了让异常更加详细、可读，想增加一些异常类型的其他功能，我们可以自定义所需异常类型。不过，大多数情况下，Python内置的异常类型就足够好了。

## **异常的使用场景与注意点**

学完了前面的基础知识，接下来我们着重谈一下，异常的使用场景与注意点。

通常来说，在程序中，如果我们不确定某段代码能否成功执行，往往这个地方就需要使用异常处理。除了上述文件读取的例子，我可以再举一个例子来说明。

大型社交网站的后台，需要针对用户发送的请求返回相应记录。用户记录往往储存在key-value结构的数据库中，每次有请求过来后，我们拿到用户的ID，并用ID查询数据库中此人的记录，就能返回相应的结果。

而数据库返回的原始数据，往往是json string的形式，这就需要我们首先对json string进行decode（解码），你可能很容易想到下面的方法：

```
import json
raw_data = queryDB(uid) # 根据用户的id，返回相应的信息
data = json.loads(raw_data)

```

这样的代码是不是就足够了呢？

要知道，在json.loads()函数中，输入的字符串如果不符合其规范，那么便无法解码，就会抛出异常，因此加上异常处理十分必要。

```
try:
    data = json.loads(raw_data)
    ....
except JSONDecodeError as err:
    print('JSONDecodeError: {}'.format(err))

```

不过，有一点切记，我们不能走向另一个极端——滥用异常处理。

比如，当你想要查找字典中某个键对应的值时，绝不能写成下面这种形式：

```
d = {'name': 'jason', 'age': 20}
try:
    value = d['dob']
    ...
except KeyError as err:
    print('KeyError: {}'.format(err))

```

诚然，这样的代码并没有bug，但是让人看了摸不着头脑，也显得很冗余。如果你的代码中充斥着这种写法，无疑对阅读、协作来说都是障碍。因此，对于flow-control（流程控制）的代码逻辑，我们一般不用异常处理。

字典这个例子，写成下面这样就很好。

```
if 'dob' in d:
    value = d['dob']
    ...

```

## **总结**

这节课， 我们一起学习了Python的异常处理及其使用场景，你需要重点掌握下面几点。

<li>
异常，通常是指程序运行的过程中遇到了错误，终止并退出。我们通常使用try except语句去处理异常，这样程序就不会被终止，仍能继续执行。
</li>
<li>
处理异常时，如果有必须执行的语句，比如文件打开后必须关闭等等，则可以放在finally  block中。
</li>
<li>
异常处理，通常用在你不确定某段代码能否成功执行，也无法轻易判断的情况下，比如数据库的连接、读取等等。正常的flow-control逻辑，不要使用异常处理，直接用条件语句解决就可以了。
</li>

## **思考题**

最后，给你留一个思考题。在异常处理时，如果try block中有多处抛出异常，需要我们使用多个try except block吗？以数据库的连接、读取为例，下面两种写法，你觉得哪种更好呢？

第一种：

```
try:
    db = DB.connect('&lt;db path&gt;') # 可能会抛出异常
    raw_data = DB.queryData('&lt;viewer_id&gt;') # 可能会抛出异常
except (DBConnectionError, DBQueryDataError) err:
    print('Error: {}'.format(err))

```

第二种：

```
try:
    db = DB.connect('&lt;db path&gt;') # 可能会抛出异常
    try:
        raw_data = DB.queryData('&lt;viewer_id&gt;')
    except DBQueryDataError as err:
         print('DB query data error: {}'.format(err))
except DBConnectionError as err:
     print('DB connection error: {}'.format(err))

```

欢迎在留言区写下你的答案，还有你今天学习的心得和疑惑，也欢迎你把这篇文章分享给你的同事、朋友。


