💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。
推荐:Linux运维老纪的首页,持续学习,不断总结,共同进步,活到老学到老
导航剑指大厂系列:全面总结 运维核心技术:系统基础、数据库、网路技术、系统安全、自动化运维、容器技术、监控工具、脚本编程、云服务等。
常用运维工具系列:常用的运维开发工具, zabbix、nagios、docker、k8s、puppet、ansible等
数据库系列:详细总结了常用数据库 mysql、Redis、MongoDB、oracle 技术点,以及工作中遇到的 mysql 问题等
懒人运维系列:总结好用的命令,解放双手不香吗?能用一个命令完成绝不用两个操作
数据结构与算法系列:总结数据结构和算法,不同类型针对性训练,提升编程思维,剑指大厂
非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。💝💝💝 ✨✨ 欢迎订阅本专栏 ✨✨
Python异常处理与程序调试
技能目标
- 掌握 Python 的异常处理
- 掌握测试和调试程序的方法
异常处理是程序中用于处理意外情况的代码段,而在代码编写的过程中,经常要进行代
码的调试和测试工作,本章将介绍 Python 语言中异常处理和程序调试的具体使用方法。
5.1 异常处理
5.1.1 异常
在人们的工作生活中,做某一件事情的时候,通常并不能很顺序的完成,在做事情的过
程中可能会有一些意外的情况发生。比如在开车上班的途中车胎被扎漏气,就需要先补好车
胎再去上班;再比如在写作业时笔坏了,就需要换一支新笔。所以当有意外情况发生时,就
需要有对应的解决方法,以便使事情能够继续做下去。对于程序来说,当要完成某一功能时,
有可能也会产生一些意外的情况,这种意外发生的情况在程序中称为异常。
示例 1:存在除数为 0 的程序代码
示例代码如下:
print('开始执行除法运算\n\n')
while True:
str1 = '输入 1 个整数作为第 1 个操作数\n'
str2 = '输入 1 个整数作为第 2 个操作数\n'
print ('开始执行除法运算\n')
op1 = int(input(str1))
op2 = int(input(str2))
result = op1 /op2
print ('%d / %d = %d' %(op1,op2,result))
仔细阅读这段代码,并没有发现问题。只是在 while 循环进行除法的计算功能。但是请
注意:当除数是 0 时,代码中的除法运算是没有意义的。所以,在这段程序运行过程中,
如果输入的第 2 个参数是 0,则会出现异常情况,输出结果如下:
开始执行除法运算
输入 1 个整数作为第 1 个操作数
3
输入 1 个整数作为第 2 个操作数
0
Traceback (most recent call last):
File "<stdin>", line 7, in <module>
ZeroDivisionError: integer division or modulo by zero
从程序输出结果中,可以发现:运行这段程序,键盘输入的第 2 个参数是 0 时,程序
会产生一个 ZeroDivisionError 异常。Python 编译器将会输出提示信息“integer division or
modulo by zero”,并终止程序的运行。就像是汽车的车胎被扎一样,需要停下车先补好车
胎才能继续开车。程序运行出现异常时,也需要做适当的处理,再继续完成所要实现的功能。
一个健壮的程序,不能因为发生异常就中断结束。
常见的异常现象有但不限于:读写文件时,文件不存在;访问数据库时,数据库管理系
统没有启动;网络连接中断;算术运算时,除数为 0;序列越界等。
异常(Exception)通常可看作是程序的错误(Error),是指程序是有缺陷(Bug)的。
错误分为语法错误和逻辑错误。
语法错误是指 Python 解释器无法解释代码,在程序执行前就可以进行纠正。逻辑错误
是因为不完整或不合法的输入导致程序执行得不到预期的结果。程序在运行时,如果 Python
解释器遇到一个错误,会停止程序的执行,并且提示一些错误信息,这就是异常。
程序开发时,很难将所有的特殊情况都处理的面面俱到,通过异常捕获可以针对突发事
件做集中的处理,从而保证程序的稳定性和健壮性。
示例 2:使用 try-except 语句捕获并处理除数为 0 的异常
示例代码如下:
print('开始执行除法运算\n\n')
while True:
str1 = '输入 1 个整数作为第 1 个操作数\n'
str2 = '输入 1 个整数作为第 2 个操作数\n'
print ('开始执行除法运算\n')
try:
#可能产生异常的代码块
op1 = int(input(str1))
op2 = int(input(str2))
result = op1 /op2
print ('%d / %d = %d' %(op1,op2,result))
except ZeroDivisionError:
#捕获除数为 0 的异常
print ('捕获除数为 0 的异常')
#结果
>>>
开始执行除法运算
输入 1 个整数作为第 1 个操作数
11
输入 1 个整数作为第 2 个操作数
0
捕获除数为 0 的异常
开始执行除法运算
输入 1 个整数作为第 1 个操作数
在示例 2 中,当输入第 2 个参数为 0 时,程序继续到下一次循环执行,也就是异常情
况得到了处理,程序并没有因为异常而终止。
这段代码对异常使用 try-except 的语法结构,对引发的异常进行捕获和处理,保证程序
能继续执行并获得正确的结果。由此得知,对异常进行处理要分为 2 个阶段,第一个阶段
是捕获可能引发的异常,第二个阶段是要对发生的异常进行及时的处理。当异常发生时,不
仅能检测到异常条件,还可以在异常发生时采取更可靠的补救措施,排除异常。
示例 1 和示例 2 中的 ZeroDivisionError 是除数为 0 的异常类,Python 中还有很多内置
的异常类,它们分别表示程序中可能发生的各种异常,如表 5-1 所示。
表 5-1 Python 内置的异常类
异常类 | 说明 | 举例 |
NameError | 尝试访问一个未声明的变量 | >>>foo |
ZeroDivisionError | 除数为零 | >>>1/0 |
SyntaxError | 解释器语法错误 | >>>for |
IndexError | 请求的索引超出序列范围 |
>>>iList=[]
>>>iList[0]
|
KeyError | 请求一个不存在的字典关键字 |
>>>idict={1:’A’,2:’b’}
>>>print idict[‘3’]
|
IOError | 输入/输出错误 | >>>fp = open(“myfile”) |
AttributeError | 尝试访问未知的对象属性 | >>>class myClass(): |
pass
>>>my = myClass()
>>>my.id
|
下面是异常发生的三种情况,示例代码如下:
>>> foo
Traceback (most recent call last):
File "<pyshell#0>", line 1, in <module>
NameError: name 'foo' is not defined
#尝试访问一个未声明的变量
>>> for
File "<stdin>", line 1
for
^
SyntaxError: invalid syntax
#解释器语法错误
>>> iList=[]
>>> iList[0]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range
#请求的索引超出序列范围
第 1 个异常类是 NameError,后面的“name 'foo' is not defined”的表示变量“foo”未定义。
第 2 个异常类是 SyntaxError,后面的“invalid syntax”是指存在语法错误。第 3 个异常类是
IndexError,后面的“list index out of range”是索引超出序列范围。可以看出,异常产生时,
都有一个对应的异常类,且后面有英文的提示信息。熟悉常见的异常类可以准确、快速的解
决问题,减少程序中的缺陷(Bug)。
5.1.2 异常处理
在 Python 中可以使用 try 语句检测异常,任何在 try 语句块里的代码都会被检测,检查
是否有异常发生。try 语句有两种主要形式 try-except 和 try-finally。
1. try-except
使用 try-except 定义异常监控,并且提供处理异常机制的语法结构如下。
语法:
try:
语句
# 被监控异常的代码块
except 异常类 [,对象]:
语句
# 异常处理的代码
当执行 try 中的语句块时,如果出现异常,会立即中断 try 语句块的执行,转到 except
语句块,将产生的异常类型与 except 语句块中的异常进行匹配。如果匹配成功,执行相应
的异常处理。如果匹配不成功,将异常传递给更高一级的 try 语句。如果异常一直没有找到
处理程序,则停止执行,抛出异常信息。
通过示例 2 的代码分析异常的处理过程
示例代码如下:
print('开始执行除法运算\n\n')
while True:
str1 = '输入 1 个整数作为第 1 个操作数\n'
str2 = '输入 1 个整数作为第 2 个操作数\n'
print ('开始执行除法运算\n')
try:
#可能产生异常的语句块
op1 = int(input(str1))
op2 = int(input(str2))
result = op1 /op2
print ('%d / %d = %d' %(op1,op2,result))
except ZeroDivisionError as e:
#捕获除数为 0 异常
print ('捕获除数为 0 的异常')
print (e)
#结果
开始执行除法运算
输入 1 个整数作为第 1 个操作数
1
输入 1 个整数作为第 2 个操作数
0
捕获除数为 0 的异常
division by zero
开始执行除法运算
输入 1 个整数作为第 1 个操作数
示例 2 的 try 语句块中的代码有可能产生异常。当执行到“result = op1 /op2”时,如果除
数为 0 会触发异常,try 语句块就中断执行,直接转到 except 语句块进行异常类型的匹配。
在 except 语句块中,如果捕获的异常对象与“ZeroDivisionError”匹配成功,则执行与之相应
的代码块。“ZeroDivisionError as e”的作用是把异常类赋值给变量 e,用于输出异常信息。
try-except 结构还可以加入 else 语句,当没有异常产生时,执行完 try 语句块后,就要
执行 else 语句块中的内容。
示例 3:使用 try-except-else 语句捕获并处理除数为 0 的异常
示例代码如下:
print('开始执行除法运算\n\n')
while True:
str1 = '输入 1 个整数作为第 1 个操作数\n'
str2 = '输入 1 个整数作为第 2 个操作数\n'
print ('开始执行除法运算\n')
try:
op1 = int(input(str1))
op2 = int(input(str2))
result = op1 /op2
except ZeroDivisionError as e:
print ('捕获除数为 0 的异常')
print (e)
else:
#try 中没有异常产生时,执行 else
print ('%d / %d = %d' %(op1,op2,result))
#结果
开始执行除法运算
输入 1 个整数作为第 1 个操作数
3
输入 1 个整数作为第 2 个操作数
2
3 / 2 = 1
开始执行除法运算
输入 1 个整数作为第 1 个操作数
示例 3 中,输入的除数不为 0 不会产生异常,执行完 try 语句块后,程序转到 else 语
句块继续执行,输出结果的语句写到了 else 语句块中。输出结果的语句放在 try 语句块中或
者在 else 语句块中,对程序的执行结果并没有什么影响。
在同一个 try 语句块中有可能产生多种类型的异常,可以使用多个 except 语句进行处
理,语法结构如下。
语法:
try:
语句
# 被监控异常的代码块
except 异常类 1 [,对象]:
语句
# 异常处理的代码
……
except 异常类 n [,对象]:
语句
# 异常处理的代码
当 try 语句块发生异常时,将产生的异常类型与 except 后面的异常类逐一进行匹配,
按先后顺序确定相匹配的异常类型后,执行对应的语句块。
示例 4:使用多个 except 捕获多个异常类型
示例代码如下:
def safe_float(obj):
try:
retval = float(obj)
except ValueError as e1:
#字符串转浮点数异常
print (e1)
retval = "非数值类型数据不能转换为 float 数据"
except TypeError as e2:
#类型转换错误错误
print (e2)
retval = "数据类型不能转换为 float"
return retval
print (safe_float('xyz'))
print (safe_float(()))
print (safe_float(200))
print (safe_float(99.9))
#结果
could not convert string to float: 'xyz'
非数值类型数据不能转换为 float 数据
float() argument must be a string or a number, not 'tuple'
数据类型不能转换为 float
200.0
99.9
从示例 4 的运行结果可以看到,当函数 safe_float()的参数是“xyz”时,会产生 ValueError
异常对象,在被“except ValueError as e1: ”捕获后,执行对应的语句块。当参数是元组”()”
时,会产生 TypeError 异常,被“except TypeError as e2:”捕获,执行对应的语句块。如果
是正确数据将不会产生异常,则执行最后的返回语句。
2. BaseException 类
Python 中,BaseException 类是所有异常的基类,也就是所有的其他异常类型都是直
接或间接继承自 BaseException 类。直接继承 BaseException 的异常类有 SystemExit、
KeyboardExit 和 Exception 等。SystemExit 是 Python 解释器请求退出,KeyboardExit 是
用户中断执行,Exception 是常规错误。前面示例中的异常类都是 Python 内置的异常类型,
它们与用户自定义异常类一样,它们的基类都是 Exception。
如果多个 except 语句块同时出现在一个 try 语句中,异常的子类应该出现在其父类之
前。因为发生异常时 except 是按顺序逐个匹配,而只执行第一个与异常类匹配的 except
语句,因此必须先子类后父类。如果父类放在了前面,当产生子类的异常时,父类对应的
except 语句会匹配成功,子类对应的 except 语句将不会有执行的机会。示例 5:捕获子类异常和父类异常
示例代码如下:
def safe_float(obj):
try:
retval = float(obj)
except Exception as e3:
retval = "有异常产生,类型不详"
except ValueError as e1:
print (e1)
retval = "非数值类型数据不能转换为 float 数据"
except TypeError as e2:
print (e2)
retval = "数据类型不能转换为 float"
return retval
print (safe_float('xyz'))
print (safe_float(()))
print (safe_float('595.99'))
print (safe_float(200))
print (safe_float(99.9))
#结果
>>>
有异常产生,类型不详
有异常产生,类型不详
595.99
200.0
99.9
示例 5 的代码首先将捕获的异常对象与 Exception 类相匹配,然后再与 ValueError 类
和 TypeError 类做匹配。由于 Exception 是所有异常类的基类,所以当发生异常时,捕获的
异常对象会首先与 Exception 类进行匹配,并执行 Exception 对应的语句块。如果对于类型
转换产生的异常,不需要针对不同的情况进行处理,那么只需要把后两个异常处理语句删除
即可。如果想针对不同的情况处理,那么就需要调整 Exception 语句的位置,把它放到所有
异常类型的后面,也就是在其前面的异常子类都不匹配时,才会与 Exception 进行匹配。
对于相同类型的异常,可以只使用一个 except 语句,把同类型的异常对象放到一个元
组中进行处理,语法结构如下。
语法:
try:
语句
# 被监控异常的代码块
except (异常类 1 [,异常类 2][,……异常类 n])[,对象]:
语句
# 异常处理的代码
示例 6:使用元组保存并处理相同类型的异常对象
示例代码如下:
def safe_float(obj):
try:
retval = float(obj)
except (ValueError ,TypeError):
retval = "参数必须是一个数值或数值字符串"
return retval
print (safe_float('xyz'))
print (safe_float(()))
print (safe_float('595.99'))
print (safe_float(200))
print (safe_float(99.9))
#结果
>>>
参数必须是一个数值或数值字符串
参数必须是一个数值或数值字符串
595.99
200.0
99.9
示例 6 的代码将 ValueError 和 TypeError 被放到了一个元组中,它们中的任何一个异第
常发生,都会被捕获。使用这种方式的前提是,异常是同类型的;否则程序的处理是有问题
的。
3. try-except-finally
try 还有一个非常重要的处理语句 finally。一个 try 语句块只能有一个 finally 语句块,它
表示无论是否发生异常,都会执行的一段代码。加入finally后,
t
ry语句会有try-except-finally、
try-except-else-finally 和 try-finally 三种形式。finally 语句块通常用来释放占用的资源,例如
关闭文件、关闭数据库连接等。语法结构如下。
语法:
try:
语句
# 被监控异常的代码块
except 异常类 1 [,对象]:
语句
# 异常处理的代码
[else:
语句] # try 语句块的代码全部成功时的操作
finally:
语句
# 无论如何都执行
当对文件进行操作后,关闭文件是必须要做的工作。不论程序运行是否正确都应该在结
束时关闭文件,因此,可以把关闭文件的代码写到 finally 中。
示例 7:使用 try-except-else-finally 进行文件的读写操作
示例代码如下:
fp = None
try:
fp = open('/usr/local/readme.txt','r+')
fp.write('12345')
except IOError:
print ('文件读写出错')
except Exception:
print ('文件操作异常')
else:fp.seek(1)
f = fp.readlines()
print (f)
finally:
fp.close()
print ('关闭文件')
#结果
['234556789']
关闭文件
顺利执行示例 7 代码,没有产生异常,最后执行到了 finally 语句块中,执行关闭文件
的语句。如果有异常产生,同样是要执行 finally 语句块,执行关闭文件的语句。因此,finally
语句块的作用是非常明显的,把释放资源的代码放在里面,可以保证这些代码一定会被执行。
5.1.3 抛出异常
在现实生活中,当完成一项工作时,碰到问题不知道怎样解决或没有权限做决断,就需
要向上一级领导反映问题。如果上一级领导也不知道怎样解决,依然要向上反映,直到某一
级别的领导可以解决。但如果最高领导还是无法解决,就需要暂停工作,思考解决办法。异
常也有类似的情况,前面的示例中产生的异常都是可以在当前程序块中解决的。但是一旦解
决不了,就需要向调用它的程序块抛出异常,寻找解决办法。比如 float()是 Python 自带的
转换为浮点数的函数,调用时只需要传递数据给它。当它无法把参数转换为浮点数时,就抛
出了异常,告诉调用者是什么原因无法转换。此时调用者就可以根据异常对象确定问题的所
在,并找到解决办法。通过抛出异常、接收并处理异常,可以实现程序的多分支处理。
程序中抛出异常使用 raise 语句,常用的语法格式如下。
语法:
raise 异常类
raise 异常类(参数或元组)
参数是指用户可以自定义的提示信息,使调用者能依此信息快速的判断并确认存在的问
题。
示例 8:要求输入的文件名不能是”_hello_”
示例代码如下:
filename = input("please input file name:")
if filename == "_hello_":
raise NameError("input file name error")
#结果
please input file name:_hello_
Traceback (most recent call last):
File "wyjx.py", line 3, in <module>
raise NameError("input file name error")
NameError: input file name error
当输入文件名是"_hello_"时,条件判断成立,执行抛出异常语句,和前面示例中的异常
形式相同,显示输出“NameError: input file name error“。此时的异常将交给上一级处理,也
就是 Python 解释器接收异常,因为程序代码没有对异常进行处理,所以最后的结果是程序
终止运行。
示例 9:捕获并处理抛出的异常
示例代码如下:
def filename():
filename = input("please input file name:")
if filename == "_hello_":
raise NameError("input file name error")
return filename
while True:
try:
#对异常进行捕获处理
filename = filename()
print ("filename is %s" %filename)
break
except NameError:
print ("please input file name again!")#结果
please input file name:_hello_
please input file name again!
please input file name:
示例 9 的函数 filename()中,如果输入的文件名是"_hello_",将会抛出 NameError 异
常。在调用 filename()时需要用 try 对它捕获进行处理,此时程序就不会终止运行,增加了
程序的健壮性。
5.2 调试和测试程序
Python 提供了内置的 pdb 模块进行程序调试,也提供了单元测试的模块 doctest。本节
将讲解这两个模块如何使用。
5.2.1 调试程序
Pdb 模块采用命令交互的方式,可以设置断点、单步执行、查看变量等,pdb 模块中的
调试函数分为两种:语句块调试函数和调试函数。
1.语句块调试函数
run()函数可以对语句块进行调试,只要把语句块作为参数执行即可进行调试,示例代
码如下:
import pdb
pdb.run('''
for i in range(1,3):
print (i)
''')
这是对 for 循环进行调试的一段代码,运行后会出现调试的命令提示,代码如下所示:
> <string>(2)<module>()
(Pdb)
然后,就可以输入命令进行调试,常用的命令如表 5-2 所示。
表 5-2 pdb 常用命令
命令/完整命令 | 描述 |
h/help | 查看命令列表 |
j/jump | 跳转到指定行 |
n/next | 执行下一条语句,不进入函数 |
r/return | 运行到函数返回 |
s/step | 执行下一条语句,遇到函数进入 |
q/quit | 退出 pdb |
b/break | 设置断点 |
2.调试函数
如果需要对函数进行调试,可以使用 runcall(),示例代码如下:
import pdb
def sum (a,b):
total = a+b
return total
pdb.runcall(sum,10,5)
pdb.runcall(sum,10,5)的含义是调试 sum()函数,后面是调用 sum()的 2 个实参。执行
后也是 pdb 的命令行模式,输入命令可以进行调试。
5.2.2 测试程序
doctest 模块提供了测试的函数,testmod()可以对 docstring 中测试用例进行测试,测
试用例使用“>>>”表示,示例 10 代码如下:
def sum(a,b):
"""
>>> sum(1,4)
5
>>> sum(100,11)
133
"""
return a+b
if __name__ == '__main__':
import doctest
doctest.testmod()
在每个”>>>”后面是调用函数的测试用例,紧跟在下面的内容是函数的返回结果。注意:
在“>>>”后面要有一个空格。这段测试用例的执行结果如下:
>>>
**********************************************************************
File "wyjx.py", line 6, in __main__.sum
Failed example:
sum(100,11)
Expected:
133
Got:
111
**********************************************************************
1 items had failures:
1 of
2 in __main__.sum
***Test Failed*** 1 failures.
TestResults(failed=1, attempted=2)
因为对 sum(100,11)指定的结果是 133,但它的函数处理结果是 111,所以显示出相应
的错误信息。如果测试用例都能通过,将没有任何的错误信息。
可以像上面示例的代码一样,把测试代码和函数写在了一个 Python 文件中,也可以把
测试代码写到单独的文本文件中,再使用 testfile()函数进行测试,示例代码如下:
sum.py
#函数文件
def sum1(a,b):
return a+b
testsum.txt
#测试代码文件
>>> from sum import sum1
>>> sum1(1,4)
5
>>> sum1(100,11)
133
test.py
#测试文件
import doctest
doctest.testfile('testsum.txt')
执行 test.py 后,测试结果与使用 testmod()函数相同。