# 第十二章 异常
## 一、认识异常
### 1、异常定义
- 什么是异常?
异常即是一个事件,该事件会在程序执行过程中发生,影响了程序的正常执行。
一般情况下,在 Python 无法正常处理程序时就会发生一个异常。异常是 Python 对象,表示一个错误。当 Python 脚本发生异常时我们需要捕获处理它,否则程序会终止执行。
- 异常抛出机制
如果我们在运行程序没有返回想要的结果,而且还抛出一个异常信息,那程序员可以根据提供的异常信息,进行恢复处理,修改代码,然后继续执行程序。
下面是一个很简单的例子,模拟报错的程序内容:
执行以上代码,结果为:
请看代码处,① 行能够正常运行,② 行就出现了报错,这时候程序就不能继续向下执行了,程序停止并抛出异常。异常的类型详细在下一小节详细解说。
### 2、异常类型
在 Python 中,各种异常错误都是类,所有的错误类型都继承于 BaseException。基于报错类型可划分为:
- SystemExit(解析器请求退出)
- KeyboardInterrupt(用户中断执行(通常是输入^C)
- GeneratorExit(生成器发生异常来通知退出)
- Exception(常规普通异常,大多数程序的报错都基于这个类)
如下图,为常见的异常错误类型。
请回看上上面的例子,执行代码返回的异常类型是:NameError,原因是使用一个还未赋予对象的变量。"name 'x' is not delined"的意思就是 x 还没有被定义。这样,通过报错,我们可以检查代码的出错原因,进而修改代码,让程序能够执行完成。
相信学习了一段时间的 Python,大家都接触过各种各样的异常报错,不能正常将程序执行完。那遇到报错时候,首先看下报错的异常类型是什么,然后检查对应的代码,找到报错原因并修改代码。这个过程,既能锻炼处理异常代码的能力,也能在错误中进步。
## 二、异常处理
### 1、异常处理
- 什么是异常处理?
当一个程序发生异常时,代表该程序在执行时出现了非正常的情况,无法再执行下去。默认情况下,程序是要终止的。如果要避免程序退出,可以使用捕获异常的方式获取这个异常的名称,再通过其他的逻辑代码让程序继续运行,这种根据异常做出的逻辑处理叫作异常处理。
- 异常为什么需要处理?
实际开发中不能直接讲代码的报错抛给用户,而是通过异常处理的形式给出提示。如果有异常不处理,程序会挂起,异常后的代码都不会执行。这样影响实际程序的使用。
- 捕获异常
在 Python 程序执行过程中发生的异常可以通过 try 语句来检测,可以把需要检测的语句放置在 try 块里面,try 块里面的语句发生的异常都会被 try 语句检测到,并抛出异常给 Python 解释器,Python 解释器会寻找能处理这一异常的代码,并把当前异常交给其处理。这一过程称为捕获异常。如果 Python 解释器找不到处理该异常的代码,Python 解释器会终止该程序的执行。
捕捉异常可以使用多种语句形式:
- try—except
- try—finally
- try—except—finally
try/except 语句用来检测 try 语句块中的错误,从而让 except 语句捕获异常信息并处理。如果你不想在异常发生时结束你的程序,只需在 try 里捕获它。 一个 try 语句可以对应一个或多个 except 语句,但只能对应一个 finally 子句。finally 子句的作用是不管异常有没有发生,该语句块的代码都会被执行。这样就可以把一些不管异常有没有发生,都必须要执行的代码放置到 finally 子句块中。
下面,逐一对以上三种捕捉异常形式进行介绍。
### 2、try...except...
异常处理结构中最常见也最基本的结构。其中 try 子句中的代码块包含可能出错的语句,而 except 子句中的代码块用来处理异常。如果 try 中的代码块没有出现异常,则继续往下执行异常处理结构后面的代码;如果出现异常并且被 except 子句捕获,则执行 except 子句中的异常处理代码;如果出现异常单没有被 except 捕获,则继续往外层抛出;如果所有层都没有捕获并处理该异常,则程序终止并将该异常抛给最终用户。
语法规则如下:
我们用一个例子来说明 try—except 是如何工作的。案例程序很简单,要求用户输入一个数字,然后把用户输入的数字输出到屏幕上。为了更好说明 try—except 捕获异常的作用,下面的代码没有使用 try—except 语句。
当用户输入的不是数字时,上面的程序将会引发 ValueError 异常,程序被终止。如下图所示。
我们当然不希望发生上面的异常,但又无法预防用户输入错误的数据。在这种情况下,可以使用try—except语句来捕获因用户输入错误的数据而发生的异常,然后对异常进行处理就可以了。
上面的代码添加了try—except语句,用于对异常进行处理。把需要检测发生异常的语句放置在try子句块中,把需要处理异常的语句放置在except子句块中。except后面的ValueError是Python提供的标准异常名称,当传入函数的参数无效时,该异常被抛出。Python提供了几十个标准异常名称,用于处理在不同情况下发生的异常。
当不清楚异常需要使用哪个标准异常名称时,可以直接使用BaseException异常名称或Exception异常名称,BaseException异常是所有异常的基类,Exception异常是常规错误的基类。上小节就列出了Python提供的常用的标准异常的名称。
注意:一个 try 语句可能包含多个except子句,分别来处理不同的特定的异常。但只有一个分支会被执行,类似else。这个可以课外尝试练习一下。
### 3、try...finally...
finally子句是无论异常是否发生,是否捕捉,都会执行的一段代码.你可以将 finally 仅仅配合try 一起使用,也可以和 try-except(else 也是可选的)一起使用。
try-finally 语句和 try-except区别在于它不是用来捕捉异常的。作为替代,它常常用维持一致的行为。我们得知无论 try 中是否有异常触发,finally 代码段都会被执行。比如,我们在执行一长串关联命令时,会有一个问题,如果当中一个命令失败了,整个命令串事实上就没有必要执行下去了。在异常发生时,我们也需要执行一些收场工作。比如 close() 方法关闭文件。这时 try...finally... 结构就可以派上用场了。
```
try:
f = open('a.txt', 'w')
f.write('Hello!')
finally:
f.close()
```
try...finally... 虽然好用,但是代码段有点长。上一节的with 语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的“清理”操作,释放资源,比如文件使用后自动关闭、线程中锁的自动获取和释放等。
```
with open('a.txt', 'w') as f:
f.write('Hello!')
```
和上面 try...finally... 实现一样的功能。但是代码精简很多。
### 4、try...except...finally
try语句块的任何一条语句抛出异常时,后面的语句将不再执行,此时的控制权已经移交给except语句块。但在一些特殊情况下,这样的处理方式会存在一些问题,例如在一段打开文件并写入数据到文件的代码中,对文件的打开、写入、关闭等操作代码都放置在try语句块中,当执行写入文件的操作抛出异常时,后面关闭文件的语句将不会被执行,从而导致一些系统资源不能被及时释放。在这样的情况下,可以使用finally子句来解决这些问题。
注意:finally是可选的选项。
### 5、try...except...else...finally
带else子句的异常处理结构是一种特殊形式的选择结构。如果try中的代码抛出了异常,并且被某个except捕获,则执行相应的异常处理代码,这种情况下不会执行else中的代码,依赖于try代码块成功执行的代码都应该放到else代码块中;如果try中的代码没有抛出任何异常,则执行else块中的代码。
工作原理:Python尝试执行try代码块中的代码;只有可能引发异常的代码才需要放在try语句中。有时候,有一些仅在try代码块成功执行时才需要运行的代码,这些代码应放在else代码块中。
注意:处理异常的时候,一定要注意异常的继承关系。
例如在 except 中如果有 StandardError,那么永远也捕获不到 ValueError,因为 ValueError 是 StandardError 的子类,如果有,也被 StandardError 的 except 给捕获了。
### 6、自定义异常
创建新的异常很简单——定义新的类,让它继承 Exception(或者任何一个已经存在的异常类型)。
下面案例,自定义一个异常:
当输入的不是数字时触发异常。
当输入的是数字时不触发异常。
## 三、断言
强制要求满足条件才能完成代码,作用:可以定位长代码错误的地方,通过断言,只执行断言之前的代码,如果代码没有错误,那就报断言那的错误,如果之前代码有错,那就会报错误的代码。
## 四、课堂练习
#### 1、编写一个计算减法的方法,当第一个数小于第二个数时,抛出“被减数不能小于减数"的异常
#### 2、定义一个函数func(filename) filename:文件的路径。函数功能:打开文件,并且返回文件内容,最后关闭,用异常来处理可能发生的错误。
## 五、上一节课堂练习答案
#### 1、写一个简单的复制文件代码:通过函数的形式,使用参数能够传递的方法,进行赋值。
#### 2、写一个能够遍历某目录下所有内容的函数:① 简单形式:文件夹里全部都是文件;② 复杂形式:文件夹里有文件夹和文件。