异常处理与程序调试

前言

  • 异常处理是程序用于处理以外情况的代码段,而在代码编写的过程中,经常要进行代码的调试和测试工作。本章将介绍 Python 语言中的异常处理和程序调试的具体使用方法。

一、异常处理

1.1、异常

  • 在人们的工作生活中,做某一件事情的时候,通常并不能很顺序的完成,在做事情的过程中可能会有一些以外的情况发生。比如在开车上班的途中轮胎被扎漏气,就需要先补好车胎再去上班;再比如在写作业时笔坏了,就需要换一支新笔。所以当有意外情况发生时,就需要有对应的解决方法,以便使事情能够继续做下去。对于程序来说,当要完成某一件功能时,有可能也会产生一些以外的情况,这种意外发生的情况在程序中成为异常。
  • 示例1:存在除数为 0 的程序代码示例如下:
print('开始执行除法运算\n\n')
while True:
    str1 = '输入 1 个整数作为第 1 个操作数\n'
    str2 = '输入 2 个整数作为第 2 个操作数\n'
    print('开始执行除法运算\n')
    op1 = int(input(str1))
    op2 = int(input(str2))
    result = op1 / op2
    print('%d / %d = %d' % (op1, op2, result))   
  • 仔细阅读这段代码,并没有发现问题。只是在 while 循环进行除法的计算功能。但是请注意:当除数是 0 时,代码中的除法运算是没有意义的。u所以,在这段代码运行过程中,如果输入的第 2 个参数是0,则会出现异常情况,输出结果如下:
开始执行除法运算

输入 1 个整数作为第 1 个操作数
3
输入 2 个整数作为第 2 个操作数
0
Traceback (most recent call last):
  File "D:\VS\Python\pythonProject\work.py", line 8, in <module>
    result = op1 / op2
             ~~~~^~~~~
ZeroDivisionError: division by zero
  • 从程序输出结果中,可以发现:运行这段程序,键盘输入的第 2 个参数是 0 时,程序会产生一个 ZeroDivisionError 异常。Python 编译器将会输出提示信息“division by zero”,并终止程序的运行。就像汽车的车胎被扎一样,需要停下车先补好车胎才能继续开车。程序运行出现异常时,也需要做适当的处理,再继续完成所要实现的功能。一个健壮的程序,不能因为发生异常就中断结束。

  • 常见的异常现象有但不限于:读写文件时,文件不存在;访问数据库时,数据库管理系统没有启动;网络连接中断;算术运算时,除数为 0;序列越界等。

  • 异常(Exception)通常可看作是程序的错误(Error),是指程序是有缺陷(Bug)的。错误分为语法错误和逻辑错误。

  • 语法错误是值Python解释器无法解释代码,在程序执行前就可以进行纠正。逻辑错误是因为不完整或不合法的输入导致程序得不到预期的结果。程序在运行时,如果 Python 解释器遇到一个错误,会停止程序的执行,并且提示一些错误信息,这就是异常。

  • 程序开发时,很难将所有的特俗情况都处理的面面俱到,通过异常捕获可以针对突发事件做集中的处理,从而保证程序的稳定性和健壮性。

  • 示例2:使用 try-except 语句捕获并处理除数为 0 的异常示例代码如下:

print('开始执行除法运算\n\n')
while True:
    str1 = '输入 1 个整数作为第 1 个操作数\n'
    str2 = '输入 2 个整数作为第 2 个操作数\n'
    try:
        print('开始执行除法运算\n')
        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
输入 2 个整数作为第 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
  • 下面是异常发生的三种情况,示例代码如下:
>>> foo
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'foo' is not defined   # 尝试访问一个未声明的变量


>>> for
  File "<stdin>", line 1
    for
       ^
SyntaxError: invalid syntax   # 解释器语法错误


>>> iList=[1,2,3]
>>> iList[10]
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)。

1.2、异常处理

  • 在 Python 中可以使用 try 语句检测异常,任何在 try 语句块里的代码都会被检测,检查是否有异常发生。try 语句有两种主要形式 try-except 和 try-finally。
1.2.1、try-except
  • 使用 try-except 定义异常监控,并且提供处理异常机制的语法结果如下。
try:
   语句 # 被监控异常的代码块
except 异常类 [,对象]:
   语句 # 异常处理的代码
  • 当执行 try 中的语句块时,如果出现异常,会立即中断 try 语句块的执行,转到 except 语句块,将产生的异常类与except 语句块中的异常进行匹配。如果匹配成功,执行相应的异常处理。如果匹配不成功,将异常传递给更高一级的 try 语句。如果异常一直没有找到处理程序,则停止执行,抛出异常信息。
  • 通过示例 2 的代码分析异常的处理过程示例代码如下:
print('开始执行除法运算\n\n')
while True:
    str1 = '输入 1 个整数作为第 1 个操作数\n'
    str2 = '输入 2 个整数作为第 2 个操作数\n'
    try:  # 可能产生异常的语句块
        print('开始执行除法运算\n')
        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
输入 2 个整数作为第 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 = '输入 2 个整数作为第 2 个操作数\n'
    try:  # 可能产生异常的语句块
        print('开始执行除法运算\n')
        op1 = int(input(str1))
        op2 = int(input(str2))
        result = op1 / op2
        # 监控的程序是正确的话就会执行 else 语句块中的内容,所以这个打印可以注释掉
        # print('%d / %d = %d' % (op1, op2, result))
    except ZeroDivisionError as e:  # 捕获除数为 0 的异常
        print('捕获除数为 0 的异常')
        print(e)
    else:   # try 中没有异常产生时,执行 else
        print('%d / %d = %d' % (op1, op2, result))
        break
# 结果
开始执行除法运算


开始执行除法运算

输入 1 个整数作为第 1 个操作数
3
输入 2 个整数作为第 2 个操作数
2
3 / 2 = 1
  • 示例 3 中,输入的触发不为0不会产生异常,执行完 try语句块后,程序转到 else 语句块继续执行,输出结果的语句写到了 else 语句块中。输出结果的语句放在 try 语句块中或者在 else 语句块中,对程序的执行结果并没有什么影响。
  • 在同一个 try 语句块中有可能产生多种类型的异常,可以使用多个 except 语句进行处理,语句结果如下。
try:
    语句 # 被监控异常的代码块
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.0))
# 结果
could not convert string to float: 'xyz'
非数值类型数据不能转换为 float 数据
float() argument must be a string or a real number, not 'tuple'
数据类型不能转换为 float
200.0
99.0
  • 从示例4的运行结果可以看到,当函数 safe_float() 的参数是"xyz"时,会产生 ValueError 异常对象,在被“except ValueError as e1:”捕获后,执行对应的语句块。当参数是元组"()"时,会产生 TypeError异常,被“except TypeError as e2:”捕获,执行对应的语句块。如果是正确数据将不会产生异常,则执行最后的返回语句。
1.2.2、BaseException 类
  • Python 中,BaseException类是所有异常的基类,也就是所有的其他异常类型都是直接或间接继续自BaseException类,直接继BaseException的异常类有 SystemEcit、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.0))
  • 备注:
    • 使用了 Exception 基类,后续的 ValueError 和 TypeError 不在匹配,绿色部分删除掉即可
有异常产生,类型不详
有异常产生,类型不详
595.99
200.0
99.0
  • 示例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.0))
# 结果
参数必须是一个数值或数值字符串
参数必须是一个数值或数值字符串
595.99
200.0
99.0
  • 示例6的代码将ValueError和TypeError被放到了一个元组中,它们中的任何一个异常发生,都会被捕获。使用这种方式的前提是,异常是同类型的;否则程序的处理是有问题的。
1.2.3、try-except-finally
  • try还有一个非常重要的处理语句 finally(最后、最终)。一个try语句块只能有一个finally语句块,它表示无论是否发生异常,都会执行的一段代码。加入 finally后,try语句会有try-except-finally、try-except-elase-finally和try-finally三种形式。finally语句块通常用来释放占用的资源,例如关闭文件、关闭数据库连接等。语句结构如下。
try:
    语句 # 被监控的代码块
except 异常类 1[,对象]:
    语句 # 异常处理的代码
else:
    语句块 # try 语句块的代码全部成功时的操作
finally:
    语句 # 无论如果都执行
  • 当文件继续宁操作后,关闭文件是必须要做的工作。不论程序运行是否正确都应该在结束是关闭文件,因此,可以把关闭文件的代码写到finally中。

  • 示例7:使用 try-except-else-finally 进行文件的读写操作示例代码如下:

[root@localhost ~]# touch /usr/local/readme.txt
[root@localhost ~]# cat aaa.py 
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('关闭文件')
  • 备注:
    • 可以将 open 语句中的打开权限修改为“r”,用于检测出一个错误
# 结果
[root@localhost ~]# python3 aaa.py 
['2345']
关闭文件
  • 顺利执行示例7代码,没有产生异常,最后执行到了 finally 语句块中,执行关闭文件的语句。如果有异常产生,同样是要执行 finally 语句块,执行关闭文件的语句。因此,finally 语句块的作用是非常明显的,把释放资源的代码放在里面,可以保证这些代码一定会被执行。

二、抛出异常

  • 在现实生活中,当完成一项工作时,碰到问题不知道怎样解决或没有权限做决断,就需要向上一级领导反映问题。如果上一级领导也不知道怎杨解决,依然要向上反映,知道某一级别的领导可以解决。但如果最高冷冻高还是无法解决,就需要暂停工作,思考解决办法。异常也有类似的情况,前面的示例中产生的异常都是可以在当前程序中解决的。但是一旦解决不了,就需要向调用它的程序块抛出异常,寻找解决办法。比如 float() Python自带的转换为浮点数的函数,调用时只需要传递数据给它。当它无法把参数转换为浮点数时。就抛出异常,告诉调用则是什么原因无法转换,可以实现程序的多分支处理。

  • 程序中抛出异常使用 raise 语句,常用的语法格式如下。

raise 异常类
raise 异常类(参数或元组)
  • 参数是指用户可以自定义的提示信息,使调用者能依此信息快速的判断并确认存在的问题。

  • 示例8:要求输入的文件名不能是_ hello _

  • 示例代码如下:

filename = input("pleae input file name: ")
if filename == "__hello__":
    raise NameError("input file name error")
# 结果
pleae input file name: __hello__
Traceback (most recent call last):
  File "D:\VS\Python\pythonProject\work.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("pleae input file name:")
    if filename == "__hello__":
        raise NameError("input file name error")
    return filename
while True:
    try:
        filename = filename()
        print("filename is %s" % filename)
    except NameError:
        print("please input file name again!")
# 结果
pleae input file name:__hello__
please input file name again!
pleae input file name:
  • 示例9的函数 filename()中,如果输入的文件名是"_ hello _",将会抛出 NameError 异常。在调用 filename()时需要用try对它捕获进行处理,此时程序就不会终止运行,增加了程序的健壮性。

三、调试和测试程序

  • Python 提供了内置的 pdb 模块进行程序调试,也提供了单元测试的模块 doctest。本节将讲解这两个模块入股哦使用。
  • Pdb 模块采用命令行交互的方式,可以设置断点、单步执行、查看变量等,pdb 模块中的调式函数分为两种:语句块调式函数和调式函数

3.1、语句块调式函数

  • run() 函数可以对语句块进行调式,只要把语句块作为参数执行即可进行调式,示例代码如下:
import pdb
pdb.run('''
for i in range(1,3):
    print(i) 
''')
  • 注意:

    • q退出、h查看帮助、c执行此程序
  • 这是对 for 循环进行调试的一段代码,运行后会出现的命令提示,代码如下所示:

> <string>(2)<module>()
(Pdb) 
  • 然后,就可以输入命令进行调试,常用的命令如表 5-2 所示。
命令/完整命令 描述
h/help 查看命令列表
b/break 设置断点
j/jump 跳转到指定行
n/next 执行下一句语句,不进入函数
r/return 运行到函数返回
s/step 执行下一条语句,遇到函数进入
q/quit 退出pdb

3.2、调试函数

  • 如果需要对函数进行调试,可以使用 runcall(),示例代码如下:
import pdb
def sum(a, b):
    total = a + b
    return total
pdb.runcall(sum,10,5)
  • 备注:
    • 可以用 s 调试
  • pdb.runcall(sum,10,5)的含义是调试 sum()函数,后面是调用sum()2个实参。执行后也是 pdb的命令行模式,输入命令可以进行调试。
Logo

2万人民币佣金等你来拿,中德社区发起者X.Lab,联合德国优秀企业对接开发项目,领取项目得佣金!!!

更多推荐