编程世界是真实世界的缩影。我们编写的Python程序,就如同真实世界的一个个的个体,也是处在一个充满危险和挑战的虚拟世界中。我们的程序可能会被不同的人来使用,因此,
错误的输入、边界条件的触发、未预料行为的操作、运行环境的异常变化等等,都可能遇到,我们的程序可能轻则没有反应,重则程序崩溃。
这时候,我们的考验就来了,
如何处理这些异常或特殊条件?如何在程序无故关闭时获取错误原因以及触发错误的代码位置?
飞哥在刚开始工作时,清楚的记得自己写的第一个软件,需要在界面上输入很多参数。交给测试人员测试时,输入一些正常的参数,软件运行和输出结果都很正常。但是,输入一些边界参数,或是重复运行多次后,软件就没有反应或是崩溃。当时不懂异常处理和捕获打印堆栈信息,测试人员过来问飞哥原因,飞哥就一脸懵逼了,感觉程序逻辑挺正常呀,不知道代码哪里出了问题。没办法,就请教部门里的老人,牛人给了发给我一个异常处理和捕获的模块以及使用示例。加上去后,真好使,程序崩溃后,打开日志就可以看到具体的那个代码文件,文件中的行代码出错,还有详细的堆栈参数信息。简直就是神器呀,飞哥很快就定位和解决了问题。
在Python程序中,
上面的这些问题和挑战,由Python中的异常处理机制来帮你守护和解决。
小伙伴掌握了Python异常处理机制的相关知识后,也能很容易的写出健壮的程序,即使出现程序崩溃,也能打印出清新的崩溃代码信息,从而快速的定位和解决问题。
不要走开,精彩继续。接下来,飞哥就带小伙伴聊聊Python的异常处理的知识。
01
什么是异常
在程序运行过程中,导致程序不能正常运行的行为或事件,称之为异常。
通常,程序发生异常时,会导致程序崩溃,也就是程序运行被终止了。比如,小伙伴平时用其它的软件时,用着用着,突然就没见了,这就是程序发生了异常导致的。
在Python中,万物皆对象,所以异常也是一个对象。异常对象包含了异常的描述等信息,通过捕获和打印这个异常对象,可以方便开发者对问题进行定位。
常见的异常的例子:访问未定义的变量、列表或元组的索引越界、除数为零、访问一个不存在的对象属性、访问一个不存在的文件。示例如下图:
02
异常处理
为了提高程序的健壮性和容错性,提升程序的用户体验,我们需要使用异常机制对异常进行处理。比如,
对可能产生异常的参数或是操作行为,提前检测,扼杀在摇篮里。
但还有一些
开发者无法预测或检测的错误会导致程序异常,应该使用try/except语句进行异常捕获。
1、
提前检测,扼杀在摇篮里;
对于一些操作,比如非法的输入参数、除数不能为0等情况,开发者是可以使用代码检测到的,可以提前检测输入,就可以不用try/except语句。可以提高代码的可读性、运行速度以及用户体验。比如下面的示例:
ef input_age(): print("欢迎来到相亲大会...") age = 18 while True: x = input("请输入你的年龄: ") # 先检测输入的字符串是否数字 # 否则就会在类型转换时发生异常 if x.isdigit(): age = int(x) break print("你的年龄:%d岁" % age)
2、
try/except语句;
有一些异常是开发者无法预测或检测的,应该使用try/except语句进行异常捕获。
基本的语法格式
如下图所示:
try/except语句工作方式如下:
1)
首先,执行try子句(在关键字try和关键字except之间的代码块)。
2)
如果没有异常发生,忽略except子句,try子句执行后结束。
3)
如果在执行try子句的过程中发生了异常,那么try子句余下的部分将被忽略。如果异常的类型和except语句中的异常类型匹配,那么对应的except子句中的异常处理语句将被执行。
4)
如果产生的异常没有对应的except语句匹配,那么这个异常将会传递给上层的try中,直到程序入口,如果还是没有被匹配的异常捕获,程序就会崩溃(终止运行)。
在捕获异常时,可以使用单个except语句;也可以使用多分支except语句;
在使用多分支except语句时,有时还要考虑捕获优先级;还可以使用顶层的Excetption类型捕获所有异常。下面分别说明:
1)
基本示例(单个except语句)
打开一个文件,在打开前,你可能先检测这个文件是否存在。但是,即使检测的时候存在,有可能在你打开文件的那个时刻,文件被其它的程序给删除了或者文件所在的磁盘存储位置出现了坏道(硬件原因导致的),从而导致文件打开异常。代码示例如下:
def open_file(): name = "来找我呀.txt" try: f = open(name, 'r') f.close() except IOError: print('cannot open', name)
2)
多分支except语句;
在Python中内置了很多标准异常,每一种异常都能找到对应的异常类型。因此,在对一段代码块进行异常捕获时,代码块可能会产生多个不同类型的异常,对不同的异常可以使用不同的处理逻辑,这时候就要用多分支except语句,每个except语句捕获一种异常。代码示例如下:
def read_money(): name = "奖金.txt" try: with open(name, 'r') as f: for line in f.readlines(): values = line.split() # 奖金金额转换为float时 # 可能出现数据转换异常 # 也可能出现索引越界 print("{}的奖金{}元".format(values[0], float(values[1]))) except IndexError: print('数据异常,请检查数据') except ValueError: print("类型转换失败,请检查数据")
3)
捕获所有异常;
虽然我们可以指定任意数量的异常,但有些异常是调用第三方模块产生的,或者有些异常不需要单独的特殊处理,只需要捕获到这些异常就可以,这时就可以使用Exception异常类型。例如上面2)中的示例可以改为下面的:
def read_money(): name = "奖金.txt" try: with open(name, 'r') as f: for line in f.readlines(): values = line.split() # 奖金金额转换为float时,可能出现转换异常 # 也可能出现索引越界 print("{}的奖金{}元".format(values[0], float(values[1]))) except Exception as e: print("异常:", e)
4)
多分支except语句优先级;
当指定多个except语句,异常产生时,是从上到下依次匹配except语句,因此,当异常类型2是异常类型1的子类时,应该按照
先「子类异常类型」后「父类异常类型」
的顺序写,如果还有
「Exception」异常类型
,应该把
「Exception」异常类型放在最后面。
也就是按照
先具体后抽象,先子类后父类
的顺序。如下代码示例:
def read_money(): name = "奖金.txt" f = None try: # 打开文件可能出现异常 f = open(name, 'r') for line in f.readlines(): values = line.split() # 奖金金额转换为float时,可能出现转换异常 print("{}的奖金{}元".format(values[0], float(values[1]))) except FileNotFoundError: # FileNotFoundError异常是IOError异常类型的子类 print('文件不存在:', name) except IOError as e: print('打开文件失败:', name, e) except Exception as e: # Exception异常类型要放在最后 print("异常:", e) finally: if f: # 防止打开文件时发生异常(此时f=None) f.close() # 关闭文件
3、
try/except/else语句;
try/except 语句还有一个可选的else语句。Else语句将在 try语句块没有发生任何异常的时候执行。使用这个语句,必须放在所有的except语句之后。
基本的语法格式
如下图所示:
代码示例如下:
def read_money(): name = "奖金.txt" try: # 打开文件可能出现异常 with open(name, 'r') as f: for line in f.readlines(): if not line: continue values = line.split() # 奖金金额转换为float时,可能出现转换异常 print("{}的奖金{}元".format(values[0], float(values[1]))) except Exception as e: print("异常:", e) else: print("所有人的奖金输出完成。")
4、
try/except/finally语句;
try/except语句还有一个可选的finally语句。无论是否发生异常都将是被最后执行的代码。
基本的语法格式
如下图所示:
代码示例如下:
def read_money(): name = "奖金.txt" f = None try: # 打开文件可能出现异常 f = open(name, 'r') for line in f.readlines(): values = line.split() # 奖金金额转换为float时,可能出现转换异常 print("{}的奖金{}元".format(values[0], float(values[1]))) except IOError as e: print('打开文件失败:', name, e) except Exception as e: # Exception异常类型要放在最后 print("异常:", e) finally: # 是否发生异常,都会最后执行这里的语句 # 通常是进行清理工作 # 例如关闭上面打开的文件 if f: # 防止打开文件时发生异常(此时f=None) f.close() # 关闭文件
03
触发异常
有时候接,收到一个异常后,我们自己处理不了,就可以把它抛给上一层的代码去处理。还有的时候,需要抛出我们自定义的异常。在Python中,使用 raise 语句抛出一个指定的异常。
基本的语法格式
如下所示:
raise [Exception]
第1个可选参数Exception, 指定要被抛出的异常类型,它必须是一个异常类型的对象或是一个异常类型(Python的标准异常或是自定义异常)。
如果捕获了一个异常,并不想去处理它,使用不带参数的 raise 语句可以再次把它抛出。
示例如下:
raise Exception("输入的年龄不能小于18") #抛出标准异常raise AgeInputError("请输入正确的年龄") # 抛出自定义异常try: raise TypeError("参数类型错误") #抛出标准异常except Exception as e: # 上面的异常会被再次抛给上一层的调用代码 raise
04
自定义异常
通过创建一个新的异常类,在程序中就可以对这个新的异常类进行触发和捕获。异常类通常继承自 Exception 类,可以直接继承或者间接继承。一个简单的示例代码如下:
# 自定义异常的通用代码class AgeInputError(Exception): def __init__(self, value): self.value = value def __str__(self): return repr(self.value) try: # 触发异常,实际工作中,异常是在另一个函数或其它模块中被触发 raise AgeInputError("请输入正确的年龄")except AgeInputError as e: # 捕获自定义异常 print("异常:", e) # 打印异常信息
05
异常堆栈信息
在实际开发过程中,当软件发生异常时,我们
不仅要知道发生的异常信息是什么,还需要知道发生异常的是具体的哪个文件名,哪行代码,调用的参数值等信息。
这时候,就需要我们的大杀器现身了,它就是
traceback模块
。通过它就可以打印出我们所需要的上述重要信息。
代码示例如下:
# 模拟一段发生异常的代码def covert_test(value1, value2): return value1 / value2if __name__ == '__main__': try: # 实际工作中,异常是在另一个函数或其它模块中被触发 covert_test(1, 0) except Exception as e: print("--" * 50) print("异常:", e) # 它只能输出是什么异常 print("--" * 50) # 一行代码打印堆栈信息 traceback.print_exc()
输出结果如下:
更多Python精彩文章、新手学习干货,欢迎一起交流学习!
END
扫码关注我们
专业提供
定制学习计划和职业规划服务
公众号:Python编程研习社