疯狂Python讲义-李刚编著-第7章异常处理
第7章 异常处理
Python 的异常机制主要依赖 try、except、else、finally 和 raise 五个关键字,其中在 try 关键字后缩进的代码简称 try 块,它里面放置的是可能引发异常的代码;在 except 后对应的是异常类型和一个代码块,用于表明该 except 块处理这种类型的代码块;在多个 except 块之后可以放一个 else 块,表明程序不出现异常时还要执行 else 块;最后还可以跟一个 finally 块,finally 块用于回收在 try 块里打开的物理资源,异常机制会保证 finally 块总被执行;而 raise 用于引发一个实际的异常,raise 可以单独作为语句使用,引发一个具体的异常对象。
7.1.异常概述
7.2.异常处理机制
Python 的异常处理机制可以让程序具有极好的容错性,让程序更加健壮。当程序运行出现意外情况时,系统会自动生成一个 Error 对象来通知程序,从而实现将 "业务实现代码" 和 "错误处理代码" 分离,提供更好的可读性。
7.2.1.使用 try...except 捕获异常
Python 异常处理机制的语法结构
try:
# 业务实现代码
...
except(Error1, Error2, ...) as e:
alert 输入不合法
goto retry
如果在执行 try 块里的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对象被提交给 Python 解释器,这个过程被称为引发异常。
当 Python 解释器收到异常对象时,会寻找能处理该异常对象的 except 块,如果找到合适的 except 块,则把该异常对象交给该 except 块处理,这个过程被称为捕获异常。如果 Python 解释器找不到捕获异常的 except 块,则运行时环境终止,Python 解释器也将退出。
提示:不管程序代码块是否处于 try 块中,甚至包括 except 块中的代码,只要执行该代码块时出现异常,系统总会自动生成一个 Error 对象。如果程序没有为这段代码定义任何的 except 块,则 Python 解释器无法找到处理该异常的 except 块,程序就在此退出。
inputStr = input("请输入您下棋的坐标,应以 x,y 的格式:\n")
while inputStr != None:
try:
x_str, y_str = inputStr.split(sep = ",")
if board[int(y_str) - 1][int(x_str) - 1] != "+":
inputStr = input("您输入的坐标点已有棋子了,请重新输入")
continue
board[int(y_str) - 1][int(x_str) - 1] == "."
except Exception:
inputStr = input("请输入的坐标不合法,请重新输入,下棋坐标应以 x,y 的格式:\n")
continue
7.2.2.异常类的继承体系
当 Python 解释器接收到异常对象时,如何为该异常对象寻找 except 块呢?注意上面程序中 except 块的 except Exception:,这意味着每个 except 块都是专门用于处理该异常类及其子类的异常实例。
当 Python 解释器接收到异常对象后,会依次判断该异常对象是否是 except 块后的异常类或其子类的实例,如果是,Python 解释器将调用该 except 块来处理该异常;否则,再次拿该异常对象和下一个 except 块里的异常类进行比较。
Python 的所有异常类都从 BaseException 派生而来,提供了丰富的异常类,这些异常类之间有严格的继承关系。
| Python 的常见异常类之间的继承关系 | |||
| 第1层 | 第2层 | 第3层 | 第4层 |
| BaseException | GeneratorExit | ||
| Exception | ArithmeticError | ZeroDivisionError | |
| FloatingPointError | |||
| OverflowError | |||
| BufferError | |||
| LookupError | IndexError | ||
| KeyError | |||
| SystemExit | |||
| KeyboardInterrupt | |||
BaseException 的主要子类就是 Exception,不管是系统的异常类,还是用户自定义的异常类,都应该从 Exception 派生。
import sys
try:
a = int(sys.argv[1])
b = int(sys.argv[2])
c = a / b
print('您输入的两个数相除的结果是:', c)
except IndexError:
print('索引错误:运行程序时输入的参数个数不够')
except ValueError:
print('数值错误:程序只能接收整数参数')
except ArithmeticError:
print('算术错误')
except Exception:
print('未知异常')
实际上,在进行异常捕获时不仅应该把 Exception 类对应的 except 块放在最后,而且所有父类异常的 except 块都应该排在子类异常的 except 块的后面(即:先处理小异常,再处理大异常)。
7.2.3.多异常捕获
Python 的一个 except 块可以捕获多种类型的异常。
在使用一个 except 块捕获多种类型的异常时,只要将多个异常类用圆括号括起来,中间用逗号隔开即可——其实就是构建多个异常类的元组。
import sys
try:
a = int(sys.argv[1])
b = int(sys.argv[2])
c = a / b
print('请输入的两个数相除的结果是:', c)
except (IndexError, ValueError, ArithmeticError):
print('程序发生了数组越界、数字格式异常、算术异常之一')
except:
print('未知异常')
7.2.4.访问异常信息
如果程序需要在 except 块中访问异常对象的相关信息,则可通过为异常对象声明变量来实现。当 Python 解释器决定调用某个 except 块来处理该异常对象时,会将异常对象赋值给 except 块后的异常变量,程序即可通过该变量来获得异常对象的相关信息。
所有的异常对象都包含了如下几个常用属性和方法
1.args:该属性返回异常的错误编号和描述字符串
2.errno:该属性返回异常的错误编号
3.strerror:该属性返回异常的描述字符串
4.with_traceback():通过过该方法可处理异常的传播轨迹信息
def foo():
try:
fis = open("a.txt")
except Exception as e:
print(e.args)
print(e.errno)
print(e.strerror)
foo()
7.2.5.else 块
在 Python 的异常处理流程中还可添加一个 else 块,当 try 块没有出现异常时,程序会执行 else 块。
s = input('请输入除数:')
try:
result = 20 / int(s)
print('20除以%s的结果是:%g' % (s, result))
except ValueError:
print('值错误,您必须输入数值')
except ArithmeticError:
print('算术错误,您不能输入0')
else:
print('没有出现异常')
但 Python 的异常处理使用 else 块绝不是多余的语法,当 try 块没有异常,而 else 块有异常时,就能体现出 else 块的作用了。
如果希望某段代码的异常能被后面的 except 块捕获,那么就应该将这段代码放在 try 块的代码之后;如果希望某段代码的异常能向外传播(不被 except 块捕获),那么就应该将这段代码放在 else 块中。
7.2.6.使用 finally 回收资源
有些时候,程序在 try 块里打开了一些物理资源(例如数据库连接、网络连接和磁盘文件等)。这些物理资源都必须被显式回收。
提示:Python 的垃圾回收机制不会回收任何物理资源,只能回收堆内存中对象所占用的内存。
为了保证一定能回收在 try 块中打开的物理资源,异常处理机制提供了 finally 块。不管 try 块中的代码是否出现异常,也不管哪一个 except 块被执行,甚至在 try 块或 except 块中执行了 return 语句,finally 块总会被执行。
语法结构如下
try:
# 业务实现代码
...
except SubException as e:
# 异常处理块 1
...
except SubException2 as e:
# 异常处理块 2
...
...
else:
# 正常处理块
finally:
# 资源回收块
...
多个 except 块必须位于 try 块之后,finally 块必须位于所有的 except 块之后
import os
def test():
fis = None
try:
fis = open("a.txt")
except OSError as e:
print(e.strerror)
return
# os._exit(1)
finally:
if fis is not None:
try:
fis.close()
except OSError as ioe:
print(ioe.strerror)
print("执行 finally 块里的资源回收!")
test()
# 输出
No such file or directory
执行 finally 块里的资源回收!
如果将 except OSError as e 下的 return 语句注释掉,取消 os.exit(1) 的注释,即在异常处理的 except 块中使用 os.exit(1) 语句来退出 Python 解释器。
No such file or directory
运行结果表明 finally 块没有被执行。如果在异常处理代码中使用 os.exit(1) 语句来退出 Python 解释器,则 finally 块将失去执行的机会。
注意:除非在 try 块、except 块中调用了退出 Python 解释器的方法,否则不管在 try 块、except 块中执行怎样的代码,出现怎样的情况,异常处理的 finally 块总会被执行。调用 sys.exit(1) 方法退出程序不能阻止 finally 块的执行,这是因为 sys.exit() 方法本身就是通过引发 SystemExit 异常来退出程序的。
在通常情况下,不要在 finally 块中使用如 return 或 raise 等导致方法中止的语句,一旦在 finally 块中使用了 return 或 raise 语句,将会导致 try 块、except 块中的 return、raise 语句失效。
def test():
try:
return True
finally:
return False
a = test()
print(a)
# 输出
False
如果 Python 程序在执行 try 块、except 块时遇到了 return 或 raise 语句,这两条语句都会导致该方法立即结束,那么系统执行这两条语句并不会结束该方法,而是去寻找该异常处理流程 finally 块,如果没有找到 finally 块,程序立即执行 return 或 raise 语句,方法中止;如果找到 finally 块,系统立即开始执行 finally 块——只有当 finally 块执行完成后,系统才会再次跳回来执行 try 块,except 块里的 return 或 raise 语句;如果在 finally 块也使用了 return 或 raise 等导致方法中止的语句,finally 块已经终止了方法,系统将不会跳回去执行 try 块、except 块里的任何代码。
注意:尽量避免在 finally 块里使用 return 或 raise 等导致方法中止的语句,否则可能出现一些很奇怪的情况。
7.2.7.异常处理嵌套
对异常处理嵌套的深度没有很明确的限制,但通常没有必要使用超过两层的嵌套异常处理,使用层次太深的嵌套异常处理没有太大必要,而且容易导致程序的可读性降低。
7.3.使用 raise 引发异常
Python 也允许程序自发引发异常,自行引发异常使用 raise 语句来完成。
7.3.1.引发异常
raise 语句有如下三种常用的用法
1.raise:单独一个 raise。该语句引发当前上下文中捕获的异常(比如在 except 块中),或默认引发 RuntimeError 异常。
2.raise 异常类:raise 后带一个异常类。该语句引发指定异常类的默认实例。
3.raise 异常对象:引发指定的异常对象。
raise 语句每次只能引发一个异常实例。
try:
x_str, y_str = inputStr.split(sep = ",")
if board[int(x_str) - 1][int(y_str) - 1] != "+":
raise
board[int(x_str) - 1][int(y_str) - 1] = "."
except Exception as e:
print(type(e))
inputStr = input("请输入的坐标不合法,请重新输入,下棋坐标应以 x,y 的格式\n")
continue
代码使用 raise 语句来自行引发异常,程序认为当用户试图向一个已有旗子的坐标点下棋时就是异常。当 Python 解释器接收到开发者自行引发的异常时,同样会中止当前的执行流,跳到该异常对应的 except 块,由该 except 块来处理该异常。也就是说,不管是系统自动引发的异常,还是程序员手动引发的异常,Python 解释器对异常的处理没有任何差别。
def main():
try:
mtd(3)
except Exception as e:
print('程序出现异常:', e)
mtd(3)
def mtd(a):
if a > 0:
raise ValueError("a 的值大于0,不符合要求")
main()
# 输出
程序出现异常: a 的值大于0,不符合要求
Traceback (most recent call last):
File "D:/django/test/20200203/1421.py", line 14, in <module>
main()
File "D:/django/test/20200203/1421.py", line 8, in main
mtd(3)
File "D:/django/test/20200203/1421.py", line 12, in mtd
raise ValueError("a 的值大于0,不符合要求")
ValueError: a 的值大于0,不符合要求
第一次调用 mtd(3) 的结果,该方法引发的异常被 except 块捕获并处理。后面的大段输出则是第二次调用 mtd(3) 的结果,由于该异常没有被 except 块捕获,因此该异常一直向上传播,知道传给 Python 解释器导致程序中止。
提示:第二调用 mtd(3) 引发的以 "File" 开头的三行输出,其实显示的就是异常的传播轨迹信息,也就是说,如果程序不对异常进行处理,Python 默认会在控制台输出异常的传播轨迹信息。
7.3.2.自定义异常类
用户自定义异常都应该继承 Exception 基类或 Exception 的子类
class AuctionException(Exception):pass
7.3.3.except 和 raise 同时使用
在实际应用中对异常可能需要更复杂的处理方式——当一个异常出现时,单靠某个方法无法完成处理该异常,必须由几个方法协作才可完全处理该异常。也就是说,在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次引发异常,让该方法的调用者也能捕获异常。
为了实现这种通过多个方法协作处理同一个异常的情形,可以在 except 块中结合 raise 语句来完成。
class AuctionException(Exception):pass
class AuctionTest:
def __init__(self, init_price):
self.init_price = init_price
def bid(self, bid_price):
d = 0.0
try:
d = float(bid_price)
except Exception as e:
print("转换出异常:", e)
# 再次引发自定义异常
raise AuctionException("竞拍价必须是数值,不能包含其他字符!")
if self.init_price > d:
raise AuctionException("竞拍价比起拍价低,不允许竞拍!")
initPrice = d
def main():
at = AuctionTest(20.4)
try:
at.bid("df")
except AuctionException as ae:
# 再次捕获到 bid() 方法中的异常,并对异常进行处理
print('main函数捕获的异常:', ae)
main()
# 输出
转换出异常: could not convert string to float: 'df'
main函数捕获的异常: 竞拍价必须是数值,不能包含其他字符!
代码对应的 except 块捕获到异常后,系统打印了该异常的字符串信息,接着引发一个 AuctionException 异常,通过该方法的调用者再次处理该 AuctionException 异常。所以程序中的 main() 函数,也就是 bid() 方法的调用者还可以再次捕获 AuctionException 异常,并将该异常的详细描述信息打印出来。
这种 except 和 raise 结合使用的情况在实际应用中非常常用。
实际应用对异常的处理通常分成两部分
1.应用后台需要通过日志来记录异常发生的详细情况
2.应用还需要根据异常向应用使用者传达某种提示
如果程序需要将原始异常的详细信息直接传播出去,Python 也允许用自定义异常对原始异常进行包装,修改如下
raise AuctionException(e)
上面就是把原始异常 e 包装成了 AuctionException 异常,这种方式也被称为异常包装或异常转译
7.3.4.raise 不需要参数
在使用 raise 语句时可以不带参数,此时 raise 语句处于 except 块中,它将会自动引发当前上下文激活的异常;否则,通常默认引发 RuntimeError 异常
class AuctionException(Exception):pass
class AuctionTest:
def __init__(self, init_price):
self.init_price = init_price
def bid(self, bid_price):
d = 0.0
try:
d = float(bid_price)
except Exception as e:
print("转换出异常:", e)
# 再次引发当前激活的异常
raise
if self.init_price > d:
raise AuctionException("竞拍价比起拍价低,不允许竞拍!")
initPrice = d
def main():
at = AuctionTest(20.4)
try:
at.bid("df")
except Exception as ae:
# 再次捕获到 bid() 方法中的异常,并对异常进行处理
print('main函数捕获的异常:', type(ae))
main()
# 输出
转换出异常: could not convert string to float: 'df'
main函数捕获的异常: <class 'ValueError'>
7.4.Python 的异常传播轨迹
异常对象提供了一个 with_traceback 用于处理异常的传播轨迹,查看异常的传播轨迹可追踪异常触发的源头,也可看到异常一路触发的轨迹。
class SelfException(Exception):pass
def main():
firstMethod()
def firstMethod():
secondMethod()
def secondMethod():
thirdMethod()
def thirdMethod():
raise SelfException("自定义异常信息")
main()
# 输出
Traceback (most recent call last):
File "D:/django/test/20200203/1522.py", line 12, in <module>
main()
File "D:/django/test/20200203/1522.py", line 5, in main
firstMethod()
File "D:/django/test/20200203/1522.py", line 7, in firstMethod
secondMethod()
File "D:/django/test/20200203/1522.py", line 9, in secondMethod
thirdMethod()
File "D:/django/test/20200203/1522.py", line 11, in thirdMethod
raise SelfException("自定义异常信息")
__main__.SelfException: 自定义异常信息
异常从 thirdMethod() 函数开始触发,传到 secondMethod() 函数,再传到 firstMethod() 函数,最后传到 main() 函数为止,这个过程就是 Python 的异常传播轨迹。
Python 专门提供了 traceback 模块来处理异常传播轨迹,使用 traceback 可以方便地处理 Python 的异常传播轨迹。
导入 traceback 模块之后,traceback 提供了如下两个常用方法
1.traceback.print_exec():将异常传播轨迹信息输出到控制台或指定文件中
2.format_exec():将异常传播轨迹信息转换成字符串
而 print_exc([limit[,file]]) 的完整形式是 print_exception(etype, value, tb[,limit[,file]])
1.etype:指定异常类型
2.value:指定异常值
3.tb:指定异常的 trackback 信息
当程序处于 except 块中时,该 except 块所捕获的异常信息可通过 sys 对象来获取,其中 sys.exc_type、sys.exc_value、sys.exc_traceback 就代表当前 except 块内的异常类型、异常值和异常传播轨迹。
简单地说,print_exc([limit[,file]]) 相当于如下
print_exception(sys.exc_type, sys.exc_value, sys.exc_tb[, limit[, file]])
4.limit:用于限制显示异常传播的层数,比如函数 A 调用函数 B,函数 B 发生了异常,如果指定 limit=1,则只显示函数 A 里面发生的异常。如果不设置 limit 参数,则默认全部显示
5.file:指定将异常传播轨迹信息输出到指定文件中。如果不指定参数,则默认输出到控制台
import traceback
class SelfException(Exception):pass
def main():
firstMethod()
def firstMethod():
secondMethod()
def secondMethod():
thirdMethod()
def thirdMethod():
raise SelfException("自定义异常信息")
try:
main()
except:
traceback.print_exc()
traceback.print_exc(file=open('log.txt', 'a'))
# 输出
Traceback (most recent call last):
File "D:/django/test/20200203/1542.py", line 15, in <module>
main()
File "D:/django/test/20200203/1542.py", line 7, in main
firstMethod()
File "D:/django/test/20200203/1542.py", line 9, in firstMethod
secondMethod()
File "D:/django/test/20200203/1542.py", line 11, in secondMethod
thirdMethod()
File "D:/django/test/20200203/1542.py", line 13, in thirdMethod
raise SelfException("自定义异常信息")
SelfException: 自定义异常信息
7.5.异常处理规则
成功的异常处理应该实现如下4个目标:
1.使程序代码混乱最小化
2.捕获并保留诊断信息
3.通知合适的人员
4.采用合适的方式结束异常活动
7.5.1.不要过度使用异常
过度使用异常主要表现在
1.把异常和普通错误混淆在一起,不再编写任何错误处理代码,而是以简单地引发异常来代替所有的错误
2.使用异常处理来代替流程控制
7.5.2.不要使用过于庞大的 try 块
正确的做法是,把大块的 try 块分割成多个可能出现异常的程序段落,并把它们放在单独的 try 块中,从而分别捕获并处理异常。
7.5.3.不要忽略捕获到的异常
7.6.本章小结
posted on 2020-02-02 12:16 herisson_pan 阅读(20) 评论(0) 收藏 举报
浙公网安备 33010602011771号