20. 异常处理

Hi,大家好。我是茶桁。
在我们日常使用Python或者其他编程语言的时候,不可避免的都会出现报错和异常。那么,我们今天就来谈谈异常。
什么是异常?
异常异常,根据名字简单理解,那就是非正常,也就是没有达到预期目标。
异常呢,其实就是一个事件,并且这个异常事件在程序的运行过程中出现,会影响程序的正常执行。而一般来说,异常被分为两种:
- 语法错误导致的异常
- 逻辑错误导致的异常
比如:
1 |
|
这个时候,系统抛出了异常,提示我们列表索引超出范畴。
这里我们需要知道,「异常」在Python中实际上也是一个对象,表示一个错误。当我们的程序无法继续正常进行时,就会被抛出。
我们来完整的看看这个报错信息:
1 |
|
Python在遇到异常之后,首先会给出一个「错误回溯」, 然后给出具体哪一句代码出现了问题。
然后在最后给出异常分类和解释。那么IndexError
告知我们,这是一个「索引错误」,并且给出了具体的描述「列出索引超出范围」。其中IndexError
是我们的异常类,
list index out of range
是我们的异常信息。
在程序运行过程中,会出现各种各样的异常类,常见标准异常类,我放在最下面作为一个附录。
如何处理异常
可预知
如果错误发生的情况是我们可以预知的,那么就可以使用流程控制进行预防处理。比如,两个数字的运算,其中一个不是数字,运算就会出错,这个时候就可以判断来预防:
1 |
|
在这一段代码中,我们使用isinstance
方法来检测第一个参数是否是第二个参数的所属类型。这是一个用来检测的方法,返回True
或者False
。那我们在if
中,只有真才会打印结果,假则会打印另外一则消息。
有些小伙伴会想,那既然知道不是整型就会出错,那前面限制传如整型不就好了,干嘛还要费劲去做非整判断。
你要知道,很多时候一个程序的编写和维护并不是单一一个人来做的,即便是一个人在做,也不能完全保证自己某个地方埋下了隐患。那么在每一段代码中,我们对可能预知的情况做妥善的预防是必须的。
不可预知
那可预知的情况我们避免了,可是在我们编写代码的时候,更多的情况是我们自己都不知道我们到底埋了什么雷,哪一段没有遵循规则或者逻辑。那这种情况就是不可预知的。
对于这种不可预知的情况我们该怎么办呢?我们又没办法预先判断。那这种情况下,我们可以使用try...except...
语句,在错误发生时进行处理。相关语法如下:
1 |
|
我们来看个示例,比如我们之前做过的一个注册、登录练习。其中我们有一段代码是要去读取列表中的所有用户。之前我们的练习中,有提到过文件不存在的情况,所以我们使用了a+
的方法,当文件不存在的时候,就新建。
那么现在,我们假设我们就用了r
的方法,当文件不存在的时候,一定会报错对吧?这个时候,我们可以使用两种方式来进行处理。
第一种方式,就可以在读取前先判断当前文件是否存在。
第二种方式,就可以使用try...except...
在错误发生的时候进行处理。
那么这里,我们用第二种方式来做一下处理:
1 |
|
可以看到,我们准确的捕获了错误,并且之后程序仍然继续往后执行了。
⚠️
try...except...
是在错误发生后进行处理,并不是提前判断。也就是说,错误其实还是发生了。这和if
实际上有根本性的区别。
try...except... 详解
首先,我们认识try...except
的一个特性,就是它可以处理指定的异常,如果引发了非指定的异常,则无法处理。比如,我们下面人为制造一个异常:
1 |
|
可以看到,我们这一段代码引发了一个ValueError
异常。
现在我们来捕获一下, 但是这次,我们为这个异常指定一个异常类再来看看,先看看正常状态下:
1 |
|
接着我们来看指定异常之后:
1 |
|
这里我们指定了一个IndexError
的异常类,显然我们之前看到了,程序报错是ValueError
异常类,两者并不匹配。所以最后依然还是报错。
那么之前我们谈到过标准的异常类,并且也知道异常实际上也就是一个对象。而我们平时在使用的时候,except
实际上就是去这个「标准的异常类」的列表里去查找,如果没有对应的异常类,它依然是无法捕获的。不过大部分时候,我们基本不会遇到标准异常类之外的异常。而这种处理指定的异常类的特性,平时也可以被我们使用。
其中一个使用方式,就是进行多分支处理异常类,不同的异常可以走不通的except
进行处理:
1 |
|
是不是和if...elif
的分支形式很像?
让我们继续,在我们说指定的异常类中,实际上会有一个万能的通用异常类。那就是Exception
。
1 |
|
基本上所有的异常,都可以走到这个异常类。在这段代码中,我们之前记得int(s1)
是属于一个ValueError
,
但是我们使用Exception
依然可以获取到这个错误。可是如果这两种异常类同时被指定的情况下会如何?
1 |
|
我们看到,就是按照程序的从上至下的顺序在执行。
所以,其实我们可以这样理解,当我们进行多分支异常类+通用异常类的时候,Exception
是最后的一个保底。
1 |
|
除此之外,try...except
是支持else
的,当try
里的代码顺利执行没有捕获到任何错误之后,还可以走到else
之中额外执行分支内的代码:
1 |
|
我们再来了解一下finally
,
这个方法是无论是否引发异常都会执行。通常情况下用于执行一些清理工作:
1 |
|
这段代码中,我们引发了一个异常,也被捕获了。但是依然执行了finally
内的代码,并且也未影响程序继续往后执行。
在我们平常写代码的过程中还有一种情况,就是我们需要自己制作一个异常信息,然后抛出。这个时候,我们需要用raise
,
来主动抛出异常。
1 |
|
除了上述的异常处理之外,其实还有另外一种方式,是直接判断逻辑是否成立,不成立抛出AssertionError
错误。就是使用assert
进行断言。它在表达式错误的时候,会直接抛出AssertionError
错误,如果表达式正确,这什么都不做。
1 |
|
自定义异常处理类
虽然系统已经给到了很多异常处理的方式,而我们在平时开发中也会经常的使用。但是实际上,很多时候我们都需要一些自己的处理要求。比如说,当异常出现的时候,我们要将异常信息写入日志,在日后我们从日志里查看日常信息或者做数据分析,就是我们最常使用的。
那我们接下来看看,如果做一个异常处理的自定义开发:
再最开始,我们需要归纳一下,我们到底要保存怎样一个格式:
1 |
|
在确定了日志格式后,我们可以进入开发了,首先我们需要导入两个所需的库
1 |
|
让我们先来人为创建一个日常,并用try
语句来捕获它:
1 |
|
这句代码报了一个ValueError
异常类。
1 |
|
没问题,我们捕获了异常并且正确的进入了except
。那么,我们可以通过traceback
模块来获取异常信息,
替换一下打印信息我们来查看一下。
1 |
|
接下来,就轮到logging
模块了。该模块定义了实现用于应用程序和库的灵活事件日志记录系统的函数和类。
1 |
|
在定义了logging
的基本信息之后,我们就可以定义一下将刚才的errormsg
写入日志了:
1 |
|
那么我们完善一下整个代码就是这样:
1 |
|
我们需要在异常出发的时候,将错误写入到日志内。那么需要将这段代码放到except
中。可是我们总不能每次都写这么长一段代码,那怎么办呢?嗯,没错,我们需要封装一个函数用于多次调用。
1 |
|
然后我们将导入库的方法也写进去,这样在我们需要的时候才会进行导入,顺便,我们将这个函数封装成一个类。就便于更多的文件调用:
1 |
|
这样,一个自定义的获取异常之后写入日常的类就定义好了,我们可以在任意地方导入并调用这个类方法,以便获取以及日后查看整个程序中的异常。
附录
标准的异常类
异常名称 | 描述 |
---|---|
BaseException | 所有异常的基类 |
SystemExit | 解释器请求退出 |
KeyboardInterrupt | 用户中断执行(通常是输入^C) |
Exception | 常规错误的基类 |
StopIteration | 迭代器没有更多的值 |
GeneratorExit | 生成器(generator)发生异常来通知退出 |
StandardError | 所有的内建标准异常的基类 |
ArithmeticError | 所有数值计算错误的基类 |
FloatingPointError | 浮点计算错误 |
OverflowError | 数值运算超出最大限制 |
ZeroDivisionError | 除(或取模)零 (所有数据类型) |
AssertionError | 断言语句失败 |
AttributeError | 对象没有这个属性 |
EOFError | 没有内建输入,到达EOF 标记 |
EnvironmentError | 操作系统错误的基类 |
IOError | 输入/输出操作失败 |
OSError | 操作系统错误 |
WindowsError | 系统调用失败 |
ImportError | 导入模块/对象失败 |
LookupError | 无效数据查询的基类 |
IndexError | 序列中没有此索引(index) |
KeyError | 映射中没有这个键 |
MemoryError | 内存溢出错误(对于Python 解释器不是致命的) |
NameError | 未声明/初始化对象 (没有属性) |
UnboundLocalError | 访问未初始化的本地变量 |
ReferenceError | 弱引用(Weak reference)试图访问已经垃圾回收了的对象 |
RuntimeError | 一般的运行时错误 |
NotImplementedError | 尚未实现的方法 |
SyntaxError | Python 语法错误 |
IndentationError | 缩进错误 |
TabError | Tab 和空格混用 |
SystemError | 一般的解释器系统错误 |
TypeError | 对类型无效的操作 |
ValueError | 传入无效的参数 |
UnicodeError | Unicode 相关的错误 |
UnicodeDecodeError | Unicode 解码时的错误 |
UnicodeEncodeError | Unicode 编码时错误 |
UnicodeTranslateError | Unicode 转换时错误 |
Warning | 警告的基类 |
DeprecationWarning | 关于被弃用的特征的警告 |
FutureWarning | 关于构造将来语义会有改变的警告 |
OverflowWarning | 旧的关于自动提升为长整型(long)的警告 |
PendingDeprecationWarning | 关于特性将会被废弃的警告 |
RuntimeWarning | 可疑的运行时行为(runtime behavior)的警告 |
SyntaxWarning | 可疑的语法的警告 |
UserWarning | 用户代码生成的警告 |
那么,这节课到这里也就结束了。各位小伙伴,下去以后记得勤加练习。下课。