在上一篇《手把手陪您学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)
<ipython-input-12-345a07f0f289> in <module>
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)
<ipython-input-13-e805a437abde> in <module>
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)
<ipython-input-16-c0989807bcdb> in <module>
----> 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)
<ipython-input-25-aaf87b00f26d> in <module>
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)
<ipython-input-28-0cf415846369> in <module>
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)
<ipython-input-29-91beadee65e4> in <module>
----> 1 pring("f")
NameError: name 'pring' is not defined
In [7]:if 1 < 2
print("g")
Out[7]: File "<ipython-input-33-2b4de2cf9d7c>", line 1
if 1 < 2
^
SyntaxError: invalid syntax
In [8]: if 1 < 2:
print("h")
Out[8]: File "<ipython-input-34-221bcba245b3>", 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)
<ipython-input-1-7e415183992c> in <module>
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》3——PyCharm的安装和配置
《手把手陪您学Python》5——Jupyter Notebook
For Fans:关注“亦说Python”公众号,回复“手36”,即可免费下载本篇文章所用示例语句。
