在上一篇《手把手陪您学Python》35——数据的存储中,我们学习了存储JSON数据和读取的方法。
今天,我们将会介绍一块比较独立的内容,也就是错误和异常处理。在我们运行程序,特别是面对用户运行程序时,能够优雅地处理错误和异常情况,这是非常重要的事情。
但可能有人会问了,既然程序都已经面向用户了,为什么还会有错误呢,各种错误和异常情况,都应该在程序开发至少是测试时就应该已经解决了啊?
大家说的没错,我们的确需要在开发和测试环节处理好程序中的错误,但这个“错误”和我们今天将要介绍的“错误和异常情况”并不是一样的。前者更多地是指我们意料之外,应该解决而没有解决的“错误”;而后者则是程序正常运行的一个部分。
如果一个程序的运行是以程序开发者为主还好,开发者可以通过测试将各种潜在的问题都提前处理好。但如果一个程序同时需要依赖用户的输入,那么用户输入什么,可能就是程序开发者难以控制的了。
比如,当需要用户输入一个数字进行计算时,用户输入了一个文字,那么程序必然会报错中断;当需要用户输入一个除数,但用户却输入了一个0,显然也会报错中断。
这种不受程序开发者控制,但又有可能出现的“错误”是程序运行过程中无法避免的“假错误”,需要我们通过某种手段去进行预先的处理或者提醒。
比如,当检测到用户应该输入数字却输入文字,或者除数输入0时,就需要提示用户重新输入,而不是程序报错中断。
如果没有提前考虑到用户各种输入情况可能带来的“假错误”的情况,而导致程序真报错中断的话,就是“真错误”了,这个是作为程序开发者应该避免的。
理解了“真假”错误之后,就让我们来看一看Python是如何优雅地处理各种错误和异常情况的。
1、 错误类型
一个程序中的错误类型有很多,除了刚才我们提到的两种,还有可能是要读取文件时文件不存在;想获取字典的key值但数据类型却是元组;想获取列表的第3个元素的值但列表长度却只为2。甚至把print()写成了pring(),if语句没有写“:”或者没有缩进,等等等等。
如果想知道程序中出现了哪种错误类型,可以通过程序报错时的提示信息来获取。
一般程序报错时,会出现Traceback的错误信息。在以前的实例中,我们也遇到过很多了,只不过当时没有具体地指出其中的错误类型。
一般来说,错误类型会显示在错误信息第一行或者最后一行的开头。如果不是Traceback类型的错误提示,一般也会显示在错误信息的开头部分,而且会比较明显。
下面我们就来看一看刚才提到的几种情况的错误类型都是什么。
In [1]: a = input()
print("{}".format(int(a) + 1))
好
Out[1]: ---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
in
1 a = input()
----> 2 print("{}".format(int(a) + 1))
ValueError: invalid literal for int() with base 10: '好'
In [2]: b = input()
print("{}".format(2 / int(b)))
0
Out[2]: ---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
in
1 b = input()
----> 2 print("{}".format(2 / int(b)))
ZeroDivisionError: division by zero
In [3]: with open("c.txt") as f:
f.read()
Out[3]: ---------------------------------------------------------------------------
FileNotFoundError Traceback (most recent call last)
in
----> 1 with open("c.txt") as f:
2 f.read()
FileNotFoundError: [Errno 2] No such file or directory: 'test.txt'
In [4]: d = (1, 2, 3)
print(d.keys())
Out[4]: ---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
in
1 d = (1, 2, 3)
----> 2 print(d.keys())
AttributeError: 'tuple' object has no attribute 'keys'
In [5]: e = [1, 2]
print(e[2])
Out[5]: ---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
in
1 e = [1, 2]
----> 2 print(e[2])
IndexError: list index out of range
In [6]: pring("f")
Out[6]: ---------------------------------------------------------------------------
NameError Traceback (most recent call last)
in
----> 1 pring("f")
NameError: name 'pring' is not defined
In [7]:if 1 < 2
print("g")
Out[7]: File "", line 1
if 1 < 2
^
SyntaxError: invalid syntax
In [8]: if 1 < 2:
print("h")
Out[8]: File "", line 2
print("h")
^
IndentationError: expected an indented block
上面列举了8种常见错误的实例,每一种都有不同的错误类型,包括ValueError、ZeroDivisionError、FileNotFoundError、AttributeError、IndexError、NameError、SyntaxError、IndentationError等等,都可以很容易地从错误信息中找到。
在一个程序中可能出现的错误类型有很多种,如果要一一列举出来比较困难,也没有必要,重要的是能够对可能出现的错误进行处理,这就是我们下面要介绍的内容。
2、try-except代码块
在Python中,try-except是专门处理异常的代码块,该代码块能够让Python执行指定的操作,并告诉Python发生异常时应该如何处理。使用try-except代码块时,即使出现异常,程序也能够继续运行,而不是像上面一样出现错误信息而中断程序的运行。
在使用try-except代码块时,可以将可能导致错误的代码放在try代码块中,如果try代码块中的代码没有出现异常,程序可以继续正常运行;如果try代码块中的代码出现了错误,Python将会自动执行except中的代码块,避免程序的错误和中断发生。
让我们看一下如何使用try-except避免上面那些异常情况的发生。
In [9]: a = input("请输入一个数字:")
try:
print("{}".format(int(a) + 1))
except:
print("输入错误,请重新输入")
Out[9]: 请输入一个数字:好
输入错误,请重新输入
我们将可能发生程序错误的打印语句print("{}".format(int(a) + 1))放在了try的代码块中,如果用户的输入没有导致异常,那么可以正常打印结果。如果用户的输入导致了异常,那么就会触发except代码块print("输入错误,请重新输入")的执行,提示用户重新输入,避免了之前出现错误信息的中断现象,这就是try-except代码块的作用。
虽然应用try-except代码块后不会报错了,但目前的程序并不完美,提示用户重新输入后程序实际上就结束了,并没有重新输入的机会。所以我们用之前学习的基础知识,再把这个程序完善一下,配合try-except代码块的应用,就能够实现比较完美的效果了。
In [10]: while True:
a = input("请输入一个数字")
try:
print("{}".format(int(a) + 1))
print("程序结束!")
break
except:
print("输入错误,请重新输入")
Out[10]: 请输入一个数字 好
输入错误,请重新输入
请输入一个数字 1
2
程序结束!
通过上面两个实例可以看到,try-except代码块能够有效处理程序中以及用户输入过程中可能出现的异常,并在用户无感的情况下,使得程序能够正常运行,并继续执行try-except代码块后面的代码。
3、else
除了try和except两个代码块外,还可以针对于未发生异常的情况,执行其他特定的代码,我们可以把这部分代码放在else代码块中。
也就是说当try部分的代码未发生异常时,继续执行的是else部分的代码。如果try部分的代码发生异常,则只执行except部分的代码,并跳过else部分的代码,去执行try-except-else代码块后的代码。
In [11]: while True:
a = input("请输入一个数字")
try:
print("{}".format(int(a) + 1))
except:
print("输入错误,请重新输入")
else:
print("程序结束!")
break
Out[11]: 请输入一个数字 1
2
程序结束!
有了else代码块后,我们就可以将之前放在try代码块中的部分语句放在else代码块了。
但是问题又来了。
这样的结构和之前没有else代码块时的效果并没有什么差别,在未发生异常的前提下,既然try部分和else部分都是执行正常情况下的语句,那么为什么要分成两部分呢?把else代码块中要执行的语句,放入try中可能出现异常的语句后面,如果不出现异常,效果不是一样的么?
没错,按照语句执行的顺序来说确实是这样的。但从try语句块的规范性用法来说,这部分语句主要是可能出现异常的代码,而不是把其他在未发生异常时需要执行的语句都放在这里。这才是try的真正含义——既然要试(try),就把可能出错的代码放在这里试一试,对于不发生异常时需要运行的语句,就放在else中去执行。
总结一下try-except-else代码块的执行过程:
Python会首先尝试try代码块中的语句,只有可能引发异常的语句才放在try代码块中。当try代码块中的代码成功执行后,Python会运行else代码块中的语句,这里的语句都是依赖try代码块成功执行的语句。如果Python尝试运行try代码块中的语句出现了错误,就会执行except代码块中的语句。最后,Python会跳出整个try-except-else代码块,继续执行后面的程序。
4、except
在except代码块中,除了可以实现出现异常执行该部分代码的作用外,还可以指定出现一种或者多种特定错误时才执行该部分代码。
比如,应该输入数字却输入文字后会出现ValueError的错误提示,那么就可以指定出现该错误类型时执行的语句。但是对于其他可能引发的错误,就可能会想没有使用try-except代码块一样报错并中断程序了。
In [12]: while True:
b = input("请输入一个数字")
try:
print("{}".format(2 / int(b)))
except ValueError: # 只处理ValueError的异常情况
print("输入错误,请输入一个数字")
else:
print("程序结束!")
break
Out[12]: 请输入一个数字 好
输入错误,请输入一个数字
请输入一个数字 0
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
in
2 b = input("请输入一个数字")
3 try:
----> 4 print("{}".format(2 / int(b)))
5 except ValueError: # 只处理ValueError的异常情况
6 print("输入错误,请输入一个数字")
ZeroDivisionError: division by zero
由于上例中仅对出现ValueError的异常情况进行了处理,当用户输入文字时,出现ValueError异常,执行except代码块,提示用户重新输入,程序继续运行。但当用户输入0时,由于出现的错误类型为ZeroDivisionError,不在except代码块的处理范围内,就会像往常一样报错并中断程序了。
如果想要针对不同的错误类型执行不同的except操作,可以一个try配合多个except语句块来使用,实现不同异常的不同处理方法,这样就能够给用户提供更有针对性的错误提示,从而指导用户正确输入。
In [13]: while True:
b = input("请输入一个数字")
try:
print("{}".format(2 / int(b)))
except ValueError:
print("输入错误,请勿输入非数字字符")
except ZeroDivisionError:
print("输入错误,0不能做除数")
else:
print("程序结束!")
break
Out[13]: 请输入一个数字 好
输入错误,请勿输入非数字字符
请输入一个数字 0
输入错误,0不能做除数
请输入一个数字 2
1.0
程序结束!
如果要指定多种错误采用同一种处理方法,可以使用元组将指定的多种错误进行列举,此时,当出现元组中任何一种错误时,都会执行相同的except语句块。
In [14]: while True:
b = input("请输入一个数字")
try:
print("{}".format(2 / int(b)))
except (ValueError, ZeroDivisionError): # 同时处理ValueError和ZeroDivisionError的异常情况
print("输入错误,请输入非0数字")
else:
print("程序结束!")
break
Out[14]: 请输入一个数字 好
输入错误,请输入非0数字
请输入一个数字 0
输入错误,请输入非0数字
请输入一个数字 2
1.0
程序结束!
此时,虽然程序可以同时处理ValueError和ZeroDivisionError的异常情况,但如果出现其他错误,还是会报错中断程序的。所以在使用except指定错误类型时,一定要确保不会超出指定的错误类型的范围。如果出现其他未指定的错误类型而导致程序中断,那么就是“真错误”了。
5、finally
finally代码块中包含的是无论是否发生异常都需要执行的语句,而且是强制执行的。
大部分情况下,其实和不放在finally语句块,而放在整个try-except代码块外的效果是一样的。但是为什么又需要这个代码块,而且后面又说了一句强制执行呢?
让我们先来看看下面的例子,并比较一下其中的异同。
In [15]: while True:
b = input("请输入一个数字")
try:
print("{}".format(2 / int(b)))
except (ValueError, ZeroDivisionError): # 同时处理ValueError和ZeroDivisionError的异常情况
print("输入错误,请输入非0数字")
else:
print("程序结束!")
break
finally:
print("谢谢您的使用!")
Out[15]: 请输入一个数字 好
输入错误,请输入非0数字
谢谢您的使用!
请输入一个数字 0
输入错误,请输入非0数字
谢谢您的使用!
请输入一个数字 2
1.0
程序结束!谢谢您的使用!
In [16]: while True:
b = input("请输入一个数字")
try:
print("{}".format(2 / int(b)))
except (ValueError, ZeroDivisionError): # 同时处理ValueError和ZeroDivisionError的异常情况
print("输入错误,请输入非0数字")
else:
print("程序结束!")
break
print("谢谢您的使用!")
Out[16]: 请输入一个数字 好
输入错误,请输入非0数字
谢谢您的使用!
请输入一个数字 0
输入错误,请输入非0数字
谢谢您的使用!
请输入一个数字 2
1.0
程序结束!
上面两个实例,一个是将print("谢谢您的使用!")放在了finally代码块中,一个是放在了try-except代码块外。
执行起来的差别就是,放在finally代码块中的时候,虽然在else代码块中执行了break语句,但finally代码块中的语句还是强制执行了,并打印出了结果;而没有放在finally代码块中的时候,else代码块制定了break语句后,整个循环就结束了,不会再执行try-except代码块外的语句了。
所以在这种存在终止程序的情况下,语句是否放在finally代码块中就存在区别了。
至于其他情况下是否同样存在区别,就需要大家在程序执行过程中仔细判断,并根据程序需要来决定是放在finally代码块中,还是放在try-except代码块外了。
6、静默失败
所谓静默失败就是在程序出现异常并执行except代码块时,并没有任何的代码要执行,此时可以用pass语句作为except代码块中的语句,如果不写而空着的话就会报错了。
静默失败主要应用于不需要告知用户出现异常的情况,比如启动一个程序但是出现了异常,此时不需要告诉用户程序出现什么异常接下来会怎么处理,而是直接进行处理,让用户感觉不到其中出现的异常。
In [17]: for i in range(-2,3):
try:
print("{}".format(2 / i))
except:
pass # 出现异常后不进行任何提示
else:
print("计算完成!")
Out[17]: -1.0
计算完成!
-2.0
计算完成!
2.0
计算完成!
1.0
计算完成!
上面的实例中,虽然出现了除数为0的情况,但是没有执行任何提示异常的语句,整个程序就像没有出现异常一样顺利地执行完了。
但是像再之前的那些实例,就不太适合使用这种静默失败的方式。特别是在和用户交互的过程中,如果没有任何错误提示但程序又不能继续运行时,用户就会很迷茫,不知道下一步该如何去做了,所以还是要根据不同的情况来决定是否使用静默失败的方式。
以上就是我们对错误和异常情况处理方法的介绍。至此,Python基础知识部分的讲解就基本结束了。下一篇,我们会对最近学习的内容做一个应用性的复习,并通过一个实例和大家一起体验一下程序重构的过程,敬请关注。
感谢阅读本文!如有任何问题,欢迎留言,一起交流讨论^_^
要阅读《手把手陪您学Python》系列文章的其他篇目,请关注公众号点击菜单选择,或点击下方链接直达。
《手把手陪您学Python》1——为什么要学Python?
《手把手陪您学Python》2——Python的安装
《手把手陪您学Python》3——PyCharm的安装和配置
《手把手陪您学Python》4——Hello World!
《手把手陪您学Python》5——Jupyter Notebook
《手把手陪您学Python》6——字符串的标识
《手把手陪您学Python》7——字符串的索引
《手把手陪您学Python》8——字符串的切片
《手把手陪您学Python》9——字符串的运算
《手把手陪您学Python》10——字符串的函数
《手把手陪您学Python》11——字符串的格式化输出
《手把手陪您学Python》12——数字
《手把手陪您学Python》13——运算
《手把手陪您学Python》14——交互式输入
《手把手陪您学Python》15——判断语句if
《手把手陪您学Python》16——循环语句while
《手把手陪您学Python》17——循环的终止
《手把手陪您学Python》18——循环语句for
《手把手陪您学Python》19——第一阶段小结
《手把手陪您学Python》20——列表
《手把手陪您学Python》21——元组
《手把手陪您学Python》22——字典
《手把手陪您学Python》23——内置序列函数
《手把手陪您学Python》24——集合
《手把手陪您学Python》25——列表推导式
《手把手陪您学Python》26——自定义函数
《手把手陪您学Python》27——自定义函数的参数
《手把手陪您学Python》28——自定义函数的返回值
《手把手陪您学Python》29——匿名函数
《手把手陪您学Python》30——模块
《手把手陪您学Python》31——文件的打开
《手把手陪您学Python》32——文件的读取
《手把手陪您学Python》33——文件的关闭
《手把手陪您学Python》34——文件的写入
《手把手陪您学Python》35——数据的存储
For Fans:关注“亦说Python”公众号,回复“手36”,即可免费下载本篇文章所用示例语句。