8. 异常处理
异常处理机制已经成为一门现代编程语言的标配,Python 语言中也提供了成熟的异常处理机制。异常机制使得程序中的异常处理代码和正常业务代码分离,保证程序代码更加优雅,并可以提高程序的健壮性。
在Python 中的异常处理机制主要依赖 try
、except
、else
、finally
和 raise
五个关键字:
try
关键字中包含的代码块简称 try 块,该代码块中是可能引发异常的代码except
后对应的是异常类型和对应的代码块,表示的是 except 块针对指定的异常类型的处理代码块else
块的代码一般放置在多个except
块之后,表示的是在不出现以上各种异常类型时要执行 else 块finally
块的代码一般放置在最后,用于回收在 try 块里打开的物理资源,异常机制会保证 finally 块总被执行raise
块的代码用于抛出一个实际的异常,raise 可以单独作为语句使用,抛出一个具体的异常对象
8.1. 什么是异常
首先我们看一个例子,如下代码所示:
# 获取两个输入
a = input("请输入:a = ")
b = input("请输入:b = ")
c = float(a)/float(b)
print("a / b = ", c)
上述代码实现了一个简单的除法的功能,两个参数 a 和 b 是依靠外部输入的,如果输入 “a = 1, b = 2”,则会得到预期的结果:
请输入:a = 1
请输入:b = 2
a / b = 0.5
但是对于外部的输入,可能会输入任意的值,如“a = aaa, b = bbb”,则会引起 ValueError
,因为 float()
函数无法将其输入转换成浮点数,此时我们可以使用 alert
或者 if
作为判断:
# 获取两个输入
a = input("请输入:a = ")
b = input("请输入:b = ")
while True:
# a 不是数值
if not a.isdigit():
a = input("请重新输入:a = ")
continue
# b 不是数值
elif not b.isdigit():
b = input("请重新输入:b = ")
continue
# 都是数值,则推出判断
else:
break
c = float(a)/float(b)
print("a / b = ", c)
通过加入判断是否是数值,解决输入是非法字符的问题。当输入为“a = 1, b = 0”,则会引起 ZeroDivisionError
,这是因为除法的分母不能为 0,这时继续增加判断,如下代码所示:
# 获取两个输入
a = input("请输入:a = ")
b = input("请输入:b = ")
while True:
# a 不是数值
if not a.isdigit():
a = input("请重新输入:a = ")
continue
# b 不是数值
elif not b.isdigit() or float(b)==0:
b = input("请重新输入:b = ")
continue
# 都是数值,则推出判断
else:
break
c = float(a)/float(b)
print("a / b = ", c)
对于一个程序设计人员来说,需要尽可能预知所有可能发生的情况,尽可能保证程序在所有糟糕的情形下也都可以运行。但在实际的环境下,情况会变得更加复杂,要使得程序能够稳定的运行,需要一种机制保证对异常的处理,这就需要用到异常处理机制。
8.2. 异常处理机制
Python 的异常处理机制可以让程序具有更好的容错性,让程序更加健壮。当程序运行出现意外情况时,系统会自动生成一个 Error 对象来通知程序,从而实现将业务代码和错误处理代码分离开,提供更好的可读性。
8.2.1. 使用 try … except 捕获异常
在 Python 语言中,提供了 try ... except
的格式用于处理异常,其中,业务代码放在 try 块中,异常处理代码放在 except 块中,Python 的异常处理机制的语法结构如下所示:
try:
# 业务代码
except (Errorl, Error2, ...) as e:
# 异常处理代码
如果在执行 try 块里的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对象被提交给 Python 解释器,这个过程被称为引发异常。当 Python 解释器收到异常对象时,会寻找能处理该异常对象的 except 块,如果找到合适的 except 块,则把该异常对象交给该 except 块处理,这个过程被称为捕获异常。如果 Python 解释器找不到捕获异常的 except 块,则运行时环境终止,Python 解释器也将退出。
Python 的一个 except 块可以捕获多种类型的异常。在使用一个 except 块捕获多种类型的异常时,只要将多个异常类用圆括号 ()
括起来,中间用逗号隔开 ,
即可。
# 获取两个输入
a = input("请输入:a = ")
b = input("请输入:b = ")
while True:
try:
c = float(a)/float(b)
print("a / b = ", c)
break
except(ValueError, ZeroDivisionError) as e:
a = input("请重新输入:a = ")
b = input("请重新输入:b = ")
8.2.2. 访问异常信息
如果程序需要在 except 块中访问异常对象的相关信息,则可通过为异常对象声明变量来实现。当 Python 解释器决定调用某个 except 块来处理该异常对象时,会将异常对象赋值给 except 块后的异常变量,程序即可通过该变量来获得异常对象的相关信息。
所有的异常对象都包含了如下几个常用属性和方法:
args
:该属性返回异常的错误编号和描述字符串errno
:该属性返回异常的错误编号strerror
:该属性返回异常的描述宇符串with_traceback()
:该方法可处理异常的传播轨迹信息
如下代码所示:
def foo():
try:
f = open("a.txt")
except Exception as e:
# 访问异常的错误编号和详细信息
print(e.args)
# 访问异常的错误编号
print(e.errno)
# 访问异常的详细信息
print(e.strerror)
foo()
上述代码调用了 Exception
对象的 args
属性,访问异常的错误编号和详细信息,最终的运行结果如下所示:
(2, 'No such file or directory')
2
No such file or directory
8.2.3. else 块
在 Python 的异常处理流程中还可添加一个 else
块,当 try
块没有出现异常时,程序会执行 else
块。 使用方法如下代码所示:
# 获取两个输入
a = input("请输入:a = ")
b = input("请输入:b = ")
while True:
try:
c = float(a)/float(b)
except(ValueError, ZeroDivisionError) as e:
a = input("请重新输入:a = ")
b = input("请重新输入:b = ")
else:
print("输入正常")
print("a / b = ", c)
break
上述代码为异常处理流程添加了 else
块,当程序中的 try
块没有出现异常时,程序就会执行 else
块。
8.2.4. 使用 finally 回收资源
如果程序在 try 块里打开了一些物理资源,如数据库连接、网络连接和打开文件等,这些物理资源都必须被显式回收。
Python 的垃圾回收机制不会回收任何物理资源,只能回收堆内存中对象所占用的内存。
那么在哪里回收这些物理资源呢?
- 如果在 try 块里进行资源回收,如果 try 块的某条语句引发了异常,该语句后的其他语句通常不会获得执行的机会,这将导致位于该语句之后的资源回收语句得不到执行。
- 如果在 except 块里资源回收,因为 except 块完全有可能得不到执行,这将导致不能及时回收这些物理资源。
为了保证一定能回收在 try 块中打开的物理资源,异常处理机制提供了 finally
块。不管 try 块中的代码是否出现异常,也不管哪一个 except 块被执行,甚至在 try 块或 except 块中执行了 return 语句,finally 块总会被执行。Python 完整的异常处理语法结构如下:
try:
# 业务代码
except Exception1 as e:
# 异常处理块l
except Exception2 as e:
# 异常处理块2
else:
# 正常处理块
finally:
# 资源回收块
在异常处理语法结构中,需要注意以下几点:
- try 块是必需的,except 块和 finally 块都是可选的
- 当存在多个 except 块时,捕获父类异常的 except 块应该位于捕获子类异常的 except 块的后面
- 若存在 try 块,则必须有 except 块和 finally 块其中的一个,或有两者都有
- 多个 except 块必须位于 try 块之后,finally 块必须位于所有的 except 块之后
具体的使用方法如下代码所示:
import os
f = None
try:
f = open("a.txt")
except OSError as e:
print(e.strerror)
finally:
# 关闭磁盘文件,因收资源
if f is not None:
try:
# 关闭资源
f.close()
except OSError as ioe:
print(ioe.strerror)
print("执行 finally 块里的资源回收")
上述代码中,在 finally 块中增加了用于回收打开的物理资源。
8.3. 使用 raise 抛出异常
当程序出现错误时,系统会自动抛出异常。除此之外,Python 也允许通过 raise
语句自行抛出异常。
8.3.1. 抛出异常
需要在程序中自行引发异常,则应使用 raise
语句。raise
语句有如下三种常用的用法:
- raise:单独一个 raise。该语句抛出当前上下文中捕获的异常(比如在 except 块中),或默认抛出 RuntimeError 异常。
- raise 异常类:raise 后带一个异常类。该语句抛出指定异常类的默认实例。
- raise 异常对象:抛出指定的异常对象。
实际上,上述三种用法最终都是要抛出一个异常实例(即使指定的是异常类,实际上也是引发该类的默认实例),raise 语句每次只能引发一个异常实例。如利用 raise
主动抛出异常处理上述的除法问题,代码如下:
# 获取两个输入
a = input("请输入:a = ")
b = input("请输入:b = ")
while True:
try:
if not a.isdigit() or not b.isdigit() or float(b) == 0:
raise
c = float(a)/float(b)
print("a / b = ", c)
break
except Exception as e:
print(e.args)
a = input("请重新输入:a = ")
b = input("请重新输入:b = ")
执行上述的代码,当输入一个异常输入时,会报如下的错误:
('No active exception to reraise',)
可见,上述代码抛出了 RuntimeError
异常,程序并跳转到异常对应的 except 块,在上述代码中由 except 块来处理该异常。这就说明,不管是系统自动抛出的异常,还是人为抛出的异常, Python 解释器对异常的处理没有任何差别。
也可以在 raise 后带一个异常类,用于抛出指定异常类的默认实例,如下代码所示:
# 获取两个输入
a = input("请输入:a = ")
b = input("请输入:b = ")
while True:
try:
if not a.isdigit() or not b.isdigit():
raise ValueError
elif float(b) == 0:
raise ZeroDivisionError
c = float(a)/float(b)
print("a / b = ", c)
break
except Exception as e:
a = input("请重新输入:a = ")
b = input("请重新输入:b = ")
8.3.2. 自定义异常类
用户自定义异常都应该继承 Exception 基类
或 Exception 的子类
,在自定义异常类时基本不需要书写更多的代码,只要指定自定义异常类的父类即可。自定义异常类的代码如下所示:
class TestException(Exception):
# exception 逻辑
从新改写上述的代码,有如下的代码:
# 自定义一个异常类
class TestException(Exception):
def __init__(self):
print("TestException")
# 获取两个输入
a = input("请输入:a = ")
b = input("请输入:b = ")
while True:
try:
if not a.isdigit() or not b.isdigit() or float(b) == 0:
raise TestException
c = float(a)/float(b)
print("a / b = ", c)
break
except Exception as e:
a = input("请重新输入:a = ")
b = input("请重新输入:b = ")
当输入一个异常输入时,会报如下的错误:
请输入:a = 1
请输入:b = 0
TestException
8.4. 本章小结
本章主要介绍了 Python 异常处理机制的相关知识,Python 的异常处理主要依赖 try
、except
、else
、finally
和 raise
五个关键字,本章详细讲解了这五个关键字的用法。本章详细介绍了异常捕获的详细处理方法,以及如何使用 raise 根据业务需求引发自定义异常。
本章需要掌握知识点:
- 熟练掌握五个关键字的正确使用
- 了解如何自行抛出异常