20. 异常处理

Hi,大家好。我是茶桁。

在我们日常使用 Python 或者其他编程语言的时候,不可避免的都会出现报错和异常。那么,我们今天就来谈谈异常。

什么是异常?

异常异常,根据名字简单理解,那就是非正常,也就是没有达到预期目标。

异常呢,其实就是一个事件,并且这个异常事件在程序的运行过程中出现,会影响程序的正常执行。而一般来说,异常被分为两种:

  1. 语法错误导致的异常
  2. 逻辑错误导致的异常

比如:

1
2
3
4
5
varlist = [1, 2, 3]
print(varlist[3])

---
IndexError: list index out of range

这个时候,系统抛出了异常,提示我们列表索引超出范畴。

这里我们需要知道,「异常」在 Python 中实际上也是一个对象,表示一个错误。当我们的程序无法继续正常进行时,就会被抛出。

我们来完整的看看这个报错信息:

1
2
3
4
5
6
7
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
Cell In[2], line 2
1 varlist = [1, 2, 3]
----> 2 print(varlist[3])

IndexError: list index out of range

Python 在遇到异常之后,首先会给出一个「错误回溯」, 然后给出具体哪一句代码出现了问题。

然后在最后给出异常分类和解释。那么IndexError告知我们,这是一个「索引错误」,并且给出了具体的描述「列出索引超出范围」。其中IndexError是我们的异常类, list index out of range是我们的异常信息。

在程序运行过程中,会出现各种各样的异常类,常见标准异常类,我放在最下面作为一个附录。

如何处理异常

可预知

如果错误发生的情况是我们可以预知的,那么就可以使用流程控制进行预防处理。比如,两个数字的运算,其中一个不是数字,运算就会出错,这个时候就可以判断来预防:

1
2
3
4
5
6
7
8
9
n2 = '3'
if isinstance(n2, int):
res = 10+n2
print(res)
else:
print('非整型。')

---
非整形

在这一段代码中,我们使用isinstance方法来检测第一个参数是否是第二个参数的所属类型。这是一个用来检测的方法,返回True或者False。那我们在if中,只有真才会打印结果,假则会打印另外一则消息。

有些小伙伴会想,那既然知道不是整型就会出错,那前面限制传如整型不就好了,干嘛还要费劲去做非整判断。

你要知道,很多时候一个程序的编写和维护并不是单一一个人来做的,即便是一个人在做,也不能完全保证自己某个地方埋下了隐患。那么在每一段代码中,我们对可能预知的情况做妥善的预防是必须的。

不可预知

那可预知的情况我们避免了,可是在我们编写代码的时候,更多的情况是我们自己都不知道我们到底埋了什么雷,哪一段没有遵循规则或者逻辑。那这种情况就是不可预知的。

对于这种不可预知的情况我们该怎么办呢?我们又没办法预先判断。那这种情况下,我们可以使用try...except...语句,在错误发生时进行处理。相关语法如下:

1
2
3
4
5
6
try:
可能发生异常错误的代码
except:
如果发生异常这进入 except 代码块进行处理

异常被捕获之后程序继续向下执行

我们来看个示例,比如我们之前做过的一个注册、登录练习。其中我们有一段代码是要去读取列表中的所有用户。之前我们的练习中,有提到过文件不存在的情况,所以我们使用了a+的方法,当文件不存在的时候,就新建。

那么现在,我们假设我们就用了r的方法,当文件不存在的时候,一定会报错对吧?这个时候,我们可以使用两种方式来进行处理。

第一种方式,就可以在读取前先判断当前文件是否存在。

第二种方式,就可以使用try...except...在错误发生的时候进行处理。

那么这里,我们用第二种方式来做一下处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 假设我们读取的文件不存在,会发生错误
try:
with open('./data/user5.txt', 'r') as fp:
res = fp.read()
print(res)
except:
print('文件不存在。')

print('程序继续运行...')

---
文件不存在。
程序继续运行...

可以看到,我们准确的捕获了错误,并且之后程序仍然继续往后执行了。

⚠️ try...except... 是在错误发生后进行处理,并不是提前判断。也就是说,错误其实还是发生了。这和if实际上有根本性的区别。

try...except... 详解

首先,我们认识try...except的一个特性,就是它可以处理指定的异常,如果引发了非指定的异常,则无法处理。比如,我们下面人为制造一个异常:

1
2
3
4
5
s1 = 'hello'
int(s1)

---
ValueError: invalid literal for int() with base 10: 'hello'

可以看到,我们这一段代码引发了一个ValueError异常。

现在我们来捕获一下, 但是这次,我们为这个异常指定一个异常类再来看看,先看看正常状态下:

1
2
3
4
5
6
7
8
try:
s1 = 'hello'
int(s1)
except:
print('程序错误。')

---
程序错误。

接着我们来看指定异常之后:

1
2
3
4
5
6
7
8
try:
s1 = 'hello'
int(s1)
except IndexError as e:
print('程序错误。')

---
ValueError: invalid literal for int() with base 10: 'hello'

这里我们指定了一个IndexError的异常类,显然我们之前看到了,程序报错是ValueError异常类,两者并不匹配。所以最后依然还是报错。

那么之前我们谈到过标准的异常类,并且也知道异常实际上也就是一个对象。而我们平时在使用的时候,except实际上就是去这个「标准的异常类」的列表里去查找,如果没有对应的异常类,它依然是无法捕获的。不过大部分时候,我们基本不会遇到标准异常类之外的异常。而这种处理指定的异常类的特性,平时也可以被我们使用。

其中一个使用方式,就是进行多分支处理异常类,不同的异常可以走不通的except进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
s1 = 'hello'
try:
s1[5] # IndexError
except IndexError as e:
print('这里是 IndexError', e)
except KeyError as e:
print('这里是 KeyError', e)
except ValueError as e:
print('这里是 ValueError', e)

---
这里是 IndexError string index out of range

是不是和if...elif的分支形式很像?

让我们继续,在我们说指定的异常类中,实际上会有一个万能的通用异常类。那就是Exception

1
2
3
4
5
6
7
8
s1 = 'world'
try:
int(s1)
except Exception as e:
print('Exception ===',e)

---
Exception === invalid literal for int() with base 10: 'world'

基本上所有的异常,都可以走到这个异常类。在这段代码中,我们之前记得int(s1)是属于一个ValueError, 但是我们使用Exception依然可以获取到这个错误。可是如果这两种异常类同时被指定的情况下会如何?

1
2
3
4
5
6
7
8
9
10
s1 = 'world'
try:
int(s1)
except Exception as e:
print('Exception ===',e)
except ValueError as e:
print('ValueError ===', e)

---
Exception === invalid literal for int() with base 10: 'world'

我们看到,就是按照程序的从上至下的顺序在执行。

所以,其实我们可以这样理解,当我们进行多分支异常类+通用异常类的时候,Exception是最后的一个保底。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
s1 = 'hello'
try:
# int(s1) # ValueError
s1[5] # IndexError
except IndexError as e:
print('IndexError',e)
except KeyError as e:
print('KeyError',e)
except ValueError as e:
print('ValueError',e)
except Exception as e:
print('Exception',e)

---
IndexError string index out of range

除此之外,try...except是支持else的,当try里的代码顺利执行没有捕获到任何错误之后,还可以走到else之中额外执行分支内的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
s1 = 'hello'
try:
str(s1)
print(s1)
except IndexError as e:
print('IndexError',e)
except ValueError as e:
print('ValueError',e)
except Exception as e:
print('Exception',e)
else:
print('try 代码块中没有引发异常时,执行')

---
hello
try 代码块中没有引发异常时,执行

我们再来了解一下finally, 这个方法是无论是否引发异常都会执行。通常情况下用于执行一些清理工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
s1 = 'hello'
try:
int(s1)
print('如果前面的代码引发了异常,这个代码块将不在继续执行。。')
except IndexError as e:
print('IndexError',e)
except ValueError as e:
print('ValueError',e)
except Exception as e:
print('Exception',e)
else:
print('try 代码块中没有引发异常时,执行')
finally:
print('无论是否引发了异常,都会执行这个代码块')

print('如果上面的代码有异常并且进行了处理,那么后面的代码将继续执行')

---
ValueError invalid literal for int() with base 10: 'hello'
无论是否引发了异常,都会执行这个代码块
如果上面的代码有异常并且进行了处理,那么后面的代码将继续执行

这段代码中,我们引发了一个异常,也被捕获了。但是依然执行了finally内的代码,并且也未影响程序继续往后执行。

在我们平常写代码的过程中还有一种情况,就是我们需要自己制作一个异常信息,然后抛出。这个时候,我们需要用raise, 来主动抛出异常。

1
2
3
4
5
6
7
try:
raise Exception('发生错误')
except Exception as e:
print('Exception', e)

---
Exception 发生错误

除了上述的异常处理之外,其实还有另外一种方式,是直接判断逻辑是否成立,不成立抛出AssertionError错误。就是使用assert进行断言。它在表达式错误的时候,会直接抛出AssertionError错误,如果表达式正确,这什么都不做。

1
2
3
4
assert 2 > 3

---
AssertionError:

自定义异常处理类

虽然系统已经给到了很多异常处理的方式,而我们在平时开发中也会经常的使用。但是实际上,很多时候我们都需要一些自己的处理要求。比如说,当异常出现的时候,我们要将异常信息写入日志,在日后我们从日志里查看日常信息或者做数据分析,就是我们最常使用的。

那我们接下来看看,如果做一个异常处理的自定义开发:

再最开始,我们需要归纳一下,我们到底要保存怎样一个格式:

1
2
3
# 日志的基本格式:
- 日期时间, 异常的级别
- 异常信息:引发的异常类别,异常的信息,文件及行号。

在确定了日志格式后,我们可以进入开发了,首先我们需要导入两个所需的库

1
2
3
# 先导入所需模块
import traceback
import logging

让我们先来人为创建一个日常,并用try语句来捕获它:

1
2
3
4
 int('aaa')

---
ValueError: invalid literal for int() with base 10: 'aaa'

这句代码报了一个ValueError异常类。

1
2
3
4
5
6
7
try:
int('aaa')
except:
print('在此进行异常的处理')

---
在此进行异常的处理

没问题,我们捕获了异常并且正确的进入了except。那么,我们可以通过traceback模块来获取异常信息, 替换一下打印信息我们来查看一下。

1
2
3
4
5
6
7
8
9
10
11
12
try:
int('aaa')
except:
# 通过 traceback 获取异常信息
errormsg = traceback.format_exc()
print(errormsg)

---
Traceback (most recent call last):
File "/var/folders/h4/7cr1cmpn7v5b3x20_9wz8m740000gn/T/ipykernel_39689/2534911191.py", line 2, in <module>
int('aaa')
ValueError: invalid literal for int() with base 10: 'aaa'

接下来,就轮到logging模块了。该模块定义了实现用于应用程序和库的灵活事件日志记录系统的函数和类。

1
2
3
4
5
logging.basicConfig(
filename = './data/error.log', # 日志存储的文件及目录
format='%(asctime)s %(levelname)s \n %(message)s', # 格式化存储的日志格式
datefmt = '%Y-%m-%d %H:%M:%S'
)

在定义了logging的基本信息之后,我们就可以定义一下将刚才的errormsg写入日志了:

1
2
# 写入日志
logging.error(traceback.format_exc())

那么我们完善一下整个代码就是这样:

1
2
3
4
5
6
7
8
logging.basicConfig(
filename = './data/error.log', # 日志存储的文件及目录
format='%(asctime)s %(levelname)s \n %(message)s', # 格式化存储的日志格式
datefmt = '%Y-%m-%d %H:%M:%S'
)

# 写入日志
logging.error(traceback.format_exc())

我们需要在异常出发的时候,将错误写入到日志内。那么需要将这段代码放到except中。可是我们总不能每次都写这么长一段代码,那怎么办呢?嗯,没错,我们需要封装一个函数用于多次调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def Myexception():
# logging 的基本配置
logging.basicConfig(
filename = './data/error.log', # 日志存储的文件及目录
format='%(asctime)s %(levelname)s \n %(message)s', # 格式化存储的日志格式
datefmt = '%Y-%m-%d %H:%M:%S'
)

# 写入日志
logging.error(traceback.format_exc())

# 使用自定义异常处理类
try:
int('bb')
except:
print('在此处进行异常的处理')
Myexception() # 在异常处理的代码块中去调用自定义异常类

然后我们将导入库的方法也写进去,这样在我们需要的时候才会进行导入,顺便,我们将这个函数封装成一个类。就便于更多的文件调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 自定义异常日志处理类
class Myexception():
def __init__(self):
import traceback
import logging

# logging 的基本配置
logging.basicConfig(
filename='./error.log',# 日志存储的文件及目录
format='%(asctime)s %(levelname)s \n %(message)s',# 格式化存储的日志格式
datefmt='%Y-%m-%d %H:%M:%S'
)
# 写入日志
logging.error(traceback.format_exc())

# 使用自定义异常处理类
try:
int('bb')
except:
print('在此处进行异常的处理')
Myexception() # 在异常处理的代码块中去调用自定义异常类

这样,一个自定义的获取异常之后写入日常的类就定义好了,我们可以在任意地方导入并调用这个类方法,以便获取以及日后查看整个程序中的异常。

附录

标准的异常类

异常名称 描述
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 用户代码生成的警告

那么,这节课到这里也就结束了。各位小伙伴,下去以后记得勤加练习。下课。

作者

Hivan Du

发布于

2023-08-15

更新于

2024-01-16

许可协议

评论