Python学习笔记(15)-黑马教程-异常

异常的概念

程序在运行时,如果Python解释器遇到了一个错误,Python解释器就会停止程序的执行,并且提示一些错误信息,这就是异常

程序停止执行并提示错误信息的这个动作我们称之为抛出异常

现在我们来了解一下Python为什么会抛出异常,看一个简单的案例:

1
2
3
4
5
6
7
8
In [6]: num = int(input("请输入一个数字 "))
请输入一个数字 a
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-6-f1f596245a67> in <module>()
----> 1 num = int(input("请输入一个数字 "))
ValueError: invalid literal for int() with base 10: 'a'

在这个案例中,我们先输入了一段代码,用于接收用户输入的整数,即num = int(input("请输入一个数字 ")),但是当我们输入了一个a后,Python的解释器就报错了,并且解释器提示,输入的这个a无法用于int()函数,它只支持10进行的转换,这就是抛出异常。这说明,即使我们输入的是一段在语法上正确的代码,但是它在执行上还有可能报错的,因为我们无法判断用户在输入内容上,用户输入的是一个数字还是字母。

因此当我们面临类似的情况时,我们就需要处理这种特殊情况,我们看一下示意图:

从示意图上我们可以知道,当程序遇到错误时,会抛出异常,这个异常就是我们在设计程序时需要考虑到的情况,这种情况是抛给最初设计程序的程序员的,程序员会针对这种情况输出一定的反馈信息,例如,我们设计程序时,如果有输入性别的选项,有人输入了一个数字,那么就是错误的,程序需要针对这种情况提前设计相应的反馈信息。

捕获异常语法

在程序开发中,如果对某些代码执行不能确定是否正确,可以增加try来捕获异常,捕获异常最简单的语法格式如下所示:

1
2
3
4
try:
尝试执行的代码
except:
出现错误的处理

这里解释一下:try是指下方编写要尝试执行的代码,也就是那些不确定是否能够正常执行的代码except指的是,try下方执行失败后,需要执行哪些代码。

捕获异常的案例

现在还是看前面的那个案例,如下所示:

1
num = int(input("请输入一个整数: "))

现在我们输入一个4,结果如下所示:

1
请输入一个整数: 4

这种情况下,程序运行是正常的,现在我们输入一个a,程序运行结果如下所示:

1
2
3
4
5
请输入一个整数: a
Traceback (most recent call last):
File "D:/netdisk/bioinfo.notes/Python/黑马教程笔记/异常/hm_01_简单的异常捕获.py", line 1, in <module>
num = int(input("请输入一个整数: "))
ValueError: invalid literal for int() with base 10: 'a'

程序运行出错。

上面还是原来的内容,现在我们使用try...except...语法实现一下类似的功能,如下所示:

1
2
3
4
5
6
7
try:
num = int(input("请输入一个整数: "))
except:
print("请输入正确的整数: ")
print("-" *50)

现在我们输入一个4结果如下所示:

1
2
请输入一个整数: 4
--------------------------------------------------

现在我们输入一个a,结果如下所示:

1
2
3
请输入一个整数: a
请输入正确的整数:
--------------------------------------------------

现在我们发现了,except下面的代码得到了执行。并且两种情况下,最后一行代码,也就是print("-" * 50)的执行不受影响。

错误类型的捕获

在程序执行时,可能遇到不同类型的异常,并且需要针对不同类型的异常,做出不同的响应,这个时候就需要捕获错误类型了,这种情况下的语法格式如下所示:

1
2
3
4
5
6
7
8
9
10
11
try:
# 尝试执行的代码
pass
except 错误类型1:
# 针对错误类型1,对应的代码处理
pass
except (错误类型2, 错误类型3):
# 针对错误类型2和3,对应的代码处理
pass
except Exception as result:
print("未知错误 %s" % result)

Python解释器抛出异常时,最后一行错误信息的第一个单词,就是错误类型。

异常类型捕获案例

我们还以前面的让用户输入整数的案例为例说明一下:

这个程序的需求为:①提示用户输入一个整数;②使用8这个整数除以刚刚用户输入的整数并输出,代码如下所示:

1
2
3
4
5
6
7
# 提示用户输入一个整数
num = int(input("输入一个整数: "))
# 使用 8 除以用户输入的整数并输入
result = 8/num
print(result)

现在我们输入一个10,看一下运行结果,如下所示:

1
2
输入一个整数: 10
0.8

现在我们再输入一个字母a,我们看一下结果,结果会出错:

1
2
3
4
5
输入一个整数: a
Traceback (most recent call last):
File "D:/netdisk/bioinfo.notes/Python/黑马教程笔记/异常/hm_02_捕获错误类型.py", line 2, in <module>
num = int(input("输入一个整数: "))
ValueError: invalid literal for int() with base 10: 'a'

但是,这个程序还有可能在num这个地方出错,因为如果用户输入的是一个0,那么result = 8 / num中除数就是0了,没有意义,如下所示:

1
2
3
4
5
输入一个整数: 0
Traceback (most recent call last):
File "D:/netdisk/bioinfo.notes/Python/黑马教程笔记/异常/hm_02_捕获错误类型.py", line 6, in <module>
result = 8/num
ZeroDivisionError: division by zero

因此针对上面的这些情况,我们都要考虑到这些异常的具体情况,针对每一种具体情况来进行设计。

现在我们还需要了解一点就是,我们如何知道错误类型,前面我们提到,当Python解释器抛出异常时,最后一行错误信息的第一个单词,就是错误类型,现在我们具体的来看一下:

前面的2种错误的第一个单词分别是ValueErrorZeroDivisionError,这就是错误类型,现在我们将这个错误类型添加到代码中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
try:
# 提示用户输入一个整数
num = int(input("输入一个整数: "))
# 使用 8 除以用户输入的整数并输入
result = 8/num
print(result)
except ZeroDivisionError:
print("除0错误")

运行代码,我们输入一个0,结果如下所示:

1
2
输入一个整数: 0
0错误

当我们添加了这些代码后,Python解释器就会抛出异常了。

现在我们再来设计针对输入字母这种情况,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try:
# 提示用户输入一个整数
num = int(input("输入一个整数: "))
# 使用 8 除以用户输入的整数并输入
result = 8/num
print(result)
except ZeroDivisionError:
print("除0错误")
except ValueError:
print("输入的不是整数")

现在我们运行代码,输入一个字母a,如下所示:

1
2
输入一个整数: a
输入的不是整数

这个案例说明了针对不止一种错误类型的处理方式。

捕获未知错误

在开发中,我们要预判到所有可能出现的错误有一定难度。如果希望程序无论出现任何错误,都不会因为Python解释器抛出异常而被终止,我们可能再增加一个except,如下所示:

1
2
except Exception as result:
print("未知错误 %s" % result)

现在我们把前面的案例中的那个除数为0的错误删除,使用这种未知错误来写一下,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try:
# 提示用户输入一个整数
num = int(input("输入一个整数: "))
# 使用 8 除以用户输入的整数并输入
result = 8/num
print(result)
except ValueError:
print("输入的不是整数")
except Exception as result:
print("未知错误 %s" % result)

运行结果如下所示:

1
2
输入一个整数: 0
未知错误 division by zero

因此,在捕获异常时,最好在最后加上这么这么一句,即except Exception as result:,其中Exception是Python是有关错误的一个类。

异常捕获的完整语法

在实际开发中,为了能够处理复杂的异常情况,完整的异常语法如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
try:
# 尝试执行的代码
pass
except 错误类型1:
# 针对错误类型1,对应的代码处理
pass
except 错误类型2:
# 针对错误类型2,对应的代码处理
pass
except (错误类型3,错误类型4):
# 针对错误类型3和4,对应的代码处理
pass
except Exception as result:
# 打印错误信息
print(result)
else:
# 没有异常才会执行的代码
pass
finally:
# 无论是否有异常,都会执行的代码
print("无论是否有异常,都会执行的代码")

其中:

  • else只有在没有异常时才会执行的代码;
  • finally无论是否有异常,都会执行的代码。

现在我们求救一下前面案例的完整捕获异常,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
try:
# 提示用户输入一个整数
num = int(input("输入一个整数: "))
# 使用 8 除以用户输入的整数并输入
result = 8/num
print(result)
except ValueError:
print("输入的不是整数")
except Exception as result:
print("未知错误 %s" % result)
else:
print("尝试成功")
finally:
print("无论是否出现错误,都会执行的代码")
print("-" * 50)

现在我们输入数字1,结果如下所示:

1
2
3
4
5
输入一个整数: 1
8.0
尝试成功
无论是否出现错误,都会执行的代码
--------------------------------------------------

从结果中我们可以发现,在没有错误出现的情况下,elsefinally下面的代码都得到了执行。

现在我们输入数字0,结果如下所示:

1
2
3
4
输入一个整数: 0
未知错误 division by zero
无论是否出现错误,都会执行的代码
--------------------------------------------------

从结果中我们可以发现,在出现错误出现的情况下,else下面的代码不执行,直接输出异常情况下的错误信息,并且finally下面的代码也得到了执行,因为finally下面的代码无论什么情况下都会执行,并且最后一行的代码print("-" * 50)也得到了执行,因为抛出异常的完整代码部分就是从try开始,到finally结束,之后的代码就都正常执行了。

异常的传递

异常的传递是指,当函数/方法执行出现异常时,会将异常传递给函数/方法的调用一方,此时程序还不会被终止,如果传递到主程序,仍然没有异常处理,这个时候,程序才会被终止。

现在我们来验证一下异常的传递,先来看一下需求:

  1. 定义函数demo1(),提示用户输入一个整数并且返回;
  2. 定义函数demo2(),调用demo1()
  3. 在主程序中调用demo2()

完整代码如下所示:

1
2
3
4
def demo1():
return int(input("输入整数: "))
print(demo1())

运行程序,我们输入一个1,结果如下所示:

1
2
输入整数: 1
1

程序运行正常。现在我们输入一个字母a,如下所示:

1
2
3
4
5
6
7
输入整数: a
Traceback (most recent call last):
File "D:/netdisk/bioinfo.notes/Python/黑马教程笔记/异常/hm_05_异常的传递.py", line 4, in <module>
print(demo1())
File "D:/netdisk/bioinfo.notes/Python/黑马教程笔记/异常/hm_05_异常的传递.py", line 2, in demo1
return int(input("输入整数: "))
ValueError: invalid literal for int() with base 10: 'a'

程序报错,最下面的错误信息是ValueError: invalid literal for int() with base 10: 'a',这是说明,输入的字母无法被int()函数识别。

我们再往上看,有line 2, in demo1 return int(input("输入整数: "))字样,也就是说错误出现在了第2行,第2行在转换整数的时候出现了问题。但是,第2行代码出现问题的时候,会把异常交给第4行,也就是解释器出现的这个信息line 4, in <module> print(demo1())。第4行代码是主程序,也就是print(demo1()),它是在调用第2行的函数,第2行代码本身是不做异常处理的,主程序中没有做异常处理,因此程序就终止了。

现在我们再改造一下原代码,如下所示:

1
2
3
4
5
6
7
8
9
def demo1():
return int(input("输入整数: "))
def demo2():
return demo1()
print(demo1())

现在我们再输入一个字母a,如下所示:

1
2
3
4
5
6
7
8
9
输入整数: a
Traceback (most recent call last):
File "D:/netdisk/bioinfo.notes/Python/黑马教程笔记/异常/hm_05_异常的传递.py", line 9, in <module>
print(demo2())
File "D:/netdisk/bioinfo.notes/Python/黑马教程笔记/异常/hm_05_异常的传递.py", line 6, in demo2
return demo1()
File "D:/netdisk/bioinfo.notes/Python/黑马教程笔记/异常/hm_05_异常的传递.py", line 2, in demo1
return int(input("输入整数: "))
ValueError: invalid literal for int() with base 10: 'a'

从错误信息中我们可以知道这些信息:

  1. 出错的代码还是第2行,提示demo1函数出现的问题;
  2. 错误信息往上,我们发现了第2行代码出错时,会把错误传递给第6行,因为第6行中的demo2()函数调用了demo1()函数,继续提示demo2出现了问题,但此时我们并没有在demo2内部针对异常进行处理,此时继续向上传递,传递到了第9行;
  3. 第9行传递到了主程序,也就是print(deom2())

从上面的结果我们可以知道,一旦出现异常,异常会一级一级向上传递,一直到主程序,直到主程序终止运行。

如果我们在实际设计程序中,如果在每一个函数中都设计异常处理,这样代码量会非常大,不现实。因此我们可以利用异常的传递性,可以在主程序中捕获异常即可,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
def demo1():
return int(input("输入整数: "))
def demo2():
return demo1()
# 利用异常的传递性,在主程序捕获异常
try:
print(demo2())
except Exception as result:
print("未知3错误 %s" % result)

现在运行程序,输入一个字母a,如下所示:

1
2
输入整数: a
未知错误 invalid literal for int() with base 10: 'a'

从结果中我们可以发现,我们只在主程序中添加了异常捕获,就不用在每个函数中对异常进行捕获了。

现在总结一下,利用异常的传递性在开发程序中的好处:可以将精力放在函数的代码逻辑上,不用花太多精力放在异常捕获上,只要函数开发完成,就可以在主程序中添加异常捕获。

抛出异常

在开发中,除了代码执行出错,Python解释会自己抛出异常之外。我们还可以根据应用程序特有的功能来主动抛出异常,现在我们来看一个案例。

例如我们要添加一个用户登录模块,里面有一个函数,这个函数的功能是:提示用户输入密码,如果长度少于8,那么就抛出异常,如下所示:

这里需要注意的是,当前这个函数只负责提示用户输入密码,如果密码长度不正确,需要其他的函数进行额外处理。

抛出异常

Python中提供了一个Exception异常类。在开发时,如果满足特定功能需求时,希望抛出异常,可以做两件事情:

  1. 创建一个Exception对象
  2. 使用raise关键字抛出异常对象

我们来看一下前面案例的需求:

  1. 定义input_password函数,提示用户输入密码;
  2. 如果用户输入长度小于8,抛出异常;
  3. 如果用户输入长度大于等于8,返回输入的密码。

现在来看一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def input_password():
# 1. 提示用户输入密码
pwd = input("请输入密码: ")
# 2. 判断密码长度 >= 8, 返回用户输入的密码
if len(pwd) >= 8:
return pwd
# 3. 如果密码 < 8,主动抛出异常
print("主动抛出异常")
# 提示用户输入密码
print(input_password())

现在运行代码,我们输入12345678,结果如下所示:

1
2
请输入密码: 123456789
123456789

现在我们输入123,如下所示:

1
2
3
请输入密码: 123
主动抛出异常
None

从结果中我们可以发现,输出了异常,并且还有一个None,这里为什么会输出None呢,这是因为在代码中,抛出异常时,并没有任何返回,所以就返回了None

现在再更改一下代码,需要创建一个Exception类对象,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def input_password():
# 1. 提示用户输入密码
pwd = input("请输入密码: ")
# 2. 判断密码长度 >= 8, 返回用户输入的密码
if len(pwd) >= 8:
return pwd
# 3. 如果密码 < 8,主动抛出异常
print("主动抛出异常")
# 第一步:创建异常对象
ex = Exception("密码长度不够")
# 第二步:主动抛出异常
raise ex
# 提示用户输入密码
print(input_password())

运行结果,我们输入123,如下所示:

1
2
3
4
5
6
7
8
请输入密码: 123
主动抛出异常
Traceback (most recent call last):
File "D:/netdisk/bioinfo.notes/Python/黑马教程笔记/异常/hm_06_抛出异常.py", line 21, in <module>
print(input_password())
File "D:/netdisk/bioinfo.notes/Python/黑马教程笔记/异常/hm_06_抛出异常.py", line 16, in input_password
raise ex
Exception: 密码长度不够

从结果我们可以看出现,程序出错的代码在第16行,也就是raise ex这一句。根据异常的传递性,继续向上看,异常传递到第22行,也就是print(input_password())这一句,此时我们使用try来改一下这句,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def input_password():
# 1. 提示用户输入密码
pwd = input("请输入密码: ")
# 2. 判断密码长度 >= 8, 返回用户输入的密码
if len(pwd) >= 8:
return pwd
# 3. 如果密码 < 8,主动抛出异常
print("主动抛出异常")
# 第一步:创建异常对象-可以使用错误信息字符串作为参数
ex = Exception("密码长度不够")
# 第二步:主动抛出异常
raise ex
# 提示用户输入密码
try:
print(input_password())
except Exception as result:
print(result)

继续运行,还是输入123,如下所示:

1
2
3
请输入密码: 123
主动抛出异常
密码长度不够

这一次我们就可以发现,运行正常。

在这个案例中我们其实就发现了,我们写了Exception这个类的对象,程序最终输出的结果就是我们定义的那个对象,即ex = Exception("密码长度不够")