第七种 异常处理
Python的异常机制主要依赖 try、except、else、finally和raise五个关键字,其中在try关键字后缩进的代码块简称try块,它里面放置的是可能引发异常的代码;在except后对应的是异常类型和一个代码块,用于表明该except块处理这种异常类型的代码块;在多个except块之后可以放一个else块,表明程序不出现异常时还要执行else块;最后还可以跟一个finally块,finally块用于回收在try块里打开的物理资源,异常处理机制会保证finally块总被执行;而raise用于引发一个实际的异常,raise可以单独作为语句使用,引发一个具体的异常对象。
异常概述
异常处理机制
异常处理机制可以让程序具有极好的容错性、让程序更加健壮。
使用try...excepy捕获异常
python的异常处理机制的语法结构:
try:
# 业务实现代码
...
except (Error1,Error2,...) as e:
alert 输入不合法
goto retry
如果在执行try块里的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对象被提交给python解释器,这个过程被称为引发异常。
当python解释器收到异常对象时,会寻找能处理该异常对象的except块,如果找到合适的except块,则把该异常对象交给该except块处理,这个过程被称为捕获异常。如果python解释器找不到捕获异常的except块,则运行时环境终止,python解释器也将退出。
# 定义棋盘的大小
BOARD_SIZE = 15
# 定义一个二维列表来充当棋盘
board = []
def initBoard() :
# 把每个元素赋为"╋",用于在控制台画出棋盘
for i in range(BOARD_SIZE) :
row = ["╋"] * BOARD_SIZE
board.append(row)
# 在控制台输出棋盘的方法
def printBoard() :
# 打印每个列表元素
for i in range(BOARD_SIZE) :
for j in range(BOARD_SIZE) :
# 打印列表元素后不换行
print(board[i][j], end="")
# 每打印完一行列表元素后输出一个换行符
print()
initBoard()
printBoard()
inputStr = input("请输入您下棋的坐标,应以x,y的格式:
")
while inputStr != None :
try:
# 将用户输入的字符串以逗号(,)作为分隔符,分隔成2个字符串
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的格式
")
continue
'''
电脑随机生成2个整数,作为电脑下棋的坐标,赋给board列表
还涉及
1.坐标的有效性,只能是数字,不能超出棋盘范围
2.下的棋的点,不能重复下棋
3.每次下棋后,需要扫描谁赢了
'''
printBoard()
inputStr = input("请输入您下棋的坐标,应以x,y的格式:
")
输出结果:
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
请输入您下棋的坐标,应以x,y的格式:
12
您输入的坐标不合法,请重新输入,下棋坐标应以x,y的格式
1,2
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
●╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
╋╋╋╋╋╋╋╋╋╋╋╋╋╋╋
请输入您下棋的坐标,应以x,y的格式:
异常类的继承体系
多个except块是为了针对不同的异常类提供不用的处理方式。
python所有的异常类都是从BaseException派生而来,提供了丰富的异常类,这些异常类之间有严格的继承关系。
如果用户要实现自定义异常,则不应该继承这个BaseException基类,而是应该继承Exception类。
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("未知异常")
输出结果:
索引错误:运行程序时输入的参数个数不够
- sys.argv[0]:通常代表正在运行的python程序名
- sys.argv[1]:代表运行程序所提供的第一个参数
- sys.argv[2]:代表运行程序所提供的第二个参数...
在进行异常捕获时不仅应该把Exception类对应的except块放在最后,而且所有父类异常的except块都应该排在子类异常的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("未知异常")
输出结果:
程序发生了数组越界、数字格式异常、算术异常之一
省略异常类的except也是合法的,它表示可捕获所有类型的异常,一般会作为异常捕获的最后一个except块。
访问异常信息
如果需要在except块中访问异常对象的相关信息,则可通过为异常对象声明变量来实现,只要在单个异常类或异常类元组(多异常捕获)之后使用as再加上异常变量即可
所有的异常对象都包含了如下几个常用属性和方法
- args:该属性返回异常的错误编号和描述字符串(该属性相当于同时返回errno属性和strerror属性)
- errno:该属性返回异常的错误编号
- strerror:该属性返回异常的描述字符串
- with_traceback():通过该方法可处理异常的传播轨迹信息
def foo():
try:
fis = open("a.txt");
except Exception as e:
# 访问异常的错误编号和详细信息
print(e.args)
# 访问异常的错误编号
print(e.errno)
# 访问异常的详细信息
print(e.strerror)
foo()
输出结果:
(2, 'No such file or directory')
2
No such file or directory
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('没有出现异常')
输出结果:
请输入除数:2
20除以2的结果是: 10
没有出现异常
请输入除数:q
值错误,您必须输入数值
else块的作用:
def else_test():
s = input('请输入除数:')
result = 20 / int(s)
print('20除以%s的结果是: %g' % (s , result))
def right_main():
try:
print('try块的代码,没有异常')
except:
print('程序出现异常')
else:
# 将else_test放在else块中
else_test()
def wrong_main():
try:
print('try块的代码,没有异常')
# 将else_test放在try块代码的后面
else_test()
except:
print('程序出现异常')
wrong_main()
right_main()
输出结果:
try块的代码,没有异常
请输入除数:0
程序出现异常
try块的代码,没有异常
请输入除数:0
Traceback (most recent call last):
File "C:Userszz.spyder-py3 emp.py", line 21, in <module>
right_main()
File "C:Userszz.spyder-py3 emp.py", line 12, in right_main
else_test()
File "C:Userszz.spyder-py3 emp.py", line 3, in else_test
result = 20 / int(s)
ZeroDivisionError: division by zero
放在 else 块中的代间所引发的异常不会被 except 块捕获。所以,如果希望某段代码的异常能被后面的 except 块捕获,那么就应该将这段代码放在 try 块的代码之后 ; 如果希望某段代码的异常能向外传播(不被 except 块捕获〉,那么就应该将这段代码放在else块中。
使用finally回收资源
有些时候,程序在try块中打开了一些物理资源(如数据库连接、网络连接和磁盘文件等),这些物理资源都必须被显示回收。
不管try块中的代码是否出现异常,也不管哪一个except块被执行,甚至在try块或except块中执行了return语句,finally块总会被执行。python完整的异常处理语法结构如下:
try:
# 业务功能代码
......
except subException as e:
# 异常处理块1
......
except subException as e:
# 异常处理块2
......
else:
# 正常处理块
finally:
# 资源回收块
...
try块是必须的,如果没有try块,则不能有后面的except块和finally块。except块和finally都是可选的,但except块和finally块至少出现其中之一,也可以同时出现。
import os
def test():
fis = None
try:
fis = open("a.txt")
except OSError as e:
print(e.strerror)
# return语句强制方法返回
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块里的资源回收!
如果在异常处理处理代码中使用os._exit(1)语句来退出python解释器,则finally块将会失去执行的机会。
注意:
除非在try块 、 except块中调用了退出 Python 解释器的方法,否则不管在 try 块、except 块中执行怎样的代码,出现怎样的情况,异常处理的 finally 块总会被执行。 调用 sys.exit() 方法退出程序不能阻止 finally 块的执行,这是因为 sys.exit()方法本身就是通过引发 SystemExit 异常来退出程序的。
一旦在finally块中使用了return或raise语句,将会导致try块、except块中的return或raise语句失效,示例:
def test():
try:
# 因为finally块中包含了return语句
# 所以下面的return语句失去作用
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块己经中止了方法,系统将不会跳回去执行t可块、except块里的任何代码。
异常处理嵌套
异常处理流程代码可以被放在任何能放可执行代码的地方,因此完整的异常处理流程既可被放在try块里,也可被放在except块里,还可被放在finally块里。
使用raise引发异常
python允许程序自发引发异常,自发引发异常使用raise语句来完成
引发异常
异常是一种很“主观”的说法,与业务需求不符而产生的异常,必须由程序员来决定引发,系统无法引发这种异常。
raise语句有三种常用方法:
- raise:单独一个raise。该语句引发当前上下文中捕获的异常(比如在except块中),或默认引发RuntimeError异常
- raise 异常类:raise后带一个异常类,该语句引发指定异常类的默认实例
- raise 异常对象:引发指定的异常对象
raise语句每次只能引发一个异常实例
import traceback
def main():
try:
# 使用try...except来捕捉异常
# 此时即使程序出现异常,也不会传播给main函数
mtd(3)
except Exception as e:
print('程序出现异常:', e)
# help(e.with_traceback)
traceback.print_exc()
# e.with_traceback(e)
# 不使用try...except捕捉异常,异常会传播出来导致程序中止
#mtd(3)
def mtd(a):
if a > 0:
raise ValueError("a的值大于0,不符合要求")
main()
输出结果:
程序出现异常: a的值大于0,不符合要求
Traceback (most recent call last):
File "C:Userszhanghu.spyder-py3 emp.py", line 6, in main
mtd(3)
File "C:Userszhanghu.spyder-py3 emp.py", line 16, in mtd
raise ValueError("a的值大于0,不符合要求")
ValueError: a的值大于0,不符合要求
如果不捕获异常,会发生什么?示例:
import traceback
def main():
mtd(3)
def mtd(a):
if a > 0:
raise ValueError("a的值大于0,不符合要求")
main()
输出结果:
Traceback (most recent call last):
File "C:Userszhanghu.spyder-py3 emp.py", line 7, in <module>
main()
File "C:Userszhanghu.spyder-py3 emp.py", line 3, in main
mtd(3)
File "C:Userszhanghu.spyder-py3 emp.py", line 6, in mtd
raise ValueError("a的值大于0,不符合要求")
ValueError: a的值大于0,不符合要求
程序既可在调用mtd(3)时使用try...except来捕获异常,这样该异常将会被except块捕获,不会传播给调用它的函数;也可直接调用mtd(3),这样该函数的异常就会直接传播给它的调用函数,如果该函数也不处理该异常,就会导致程序中止。
自定义异常类
用户自定义异常都应该继承Exception基类或Exception的子类,在自定义异常类时基本不需要书写更多的代码,只需要指定自定义异常类的父类即可。
class AuctionException(Exception): pass
该异常类不需要类体定义,使用pass语句作为占位符即可
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("竞拍价必须是数值,不能包含其他字符!") # ①
raise AuctionException(e)
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函数捕捉的异常: could not convert string to float: 'df'
这种except和raise结合使用的情况在实际应用中非常常用。实际应用对异常的处理通常分成两个部分:①应用后台需要通过日志来记录异常发生的详细情况;②应用还需要根据异常向应用使用者传达某种提示。在这种情形下,所有异常都需要两个方法共同完成,也就必须将except和raise结合使用。
如果程序需要将原始异常的详细信息直接传播出去,Python也允许用自定义异常对原始异常进行包装,只要将上面①号粗体字代码改为如下形式。
raise AuctionException(e)
上面就是把原始异常e包装成了AuctionException异常,这种方式也被称为异常包装或异常转译。
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 AuctionException("竞拍价必须是数值,不能包含其他字符!") # ①
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函数捕捉的异常:', ae)
main()
输出结果:
转换出异常: could not convert string to float: 'df'
main函数捕捉的异常: could not convert string to float: 'df'
此时main()函数再次捕获了ValueError --- 它就是在bid()方法中except块所捕获的原始异常。
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 "C:Userszz.spyder-py3 emp.py", line 11, in <module>
main()
File "C:Userszz.spyder-py3 emp.py", line 4, in main
firstMethod()
File "C:Userszz.spyder-py3 emp.py", line 6, in firstMethod
secondMethod()
File "C:Userszz.spyder-py3 emp.py", line 8, in secondMethod
thirdMethod()
File "C:Userszz.spyder-py3 emp.py", line 10, in thirdMethod
raise SelfException("自定义异常信息")
SelfException: 自定义异常信息
异常从thirdMethod()函数开始触发,传到secondMethod()函数,再传到firstMethod()函数,最后传到main()函数,在main()函数止,这个过程就是Python的异常传播轨迹。
在实际应用程序的开发中,大多数复杂操作都会被分解成一系列函数或方法调用。这是因为:为了具有更好的可重用性,会将每个可重用的代码单元定义成函数或方法,将复杂任务逐渐分解为更易管理的小型子任务。由于一个大的业务功能需要由多个函数或方法来共同实现,在最终编程模型中,很多对象将通过一系列函数或方法调用来实现通信,执行任务。所以,当应用程序运行时,经常会发生一系列函数或方法调用,从而形成“函数调用栈”。异常的传播则相反:只要异常没有被完全捕获(包括异常没有被捕获,或者异常被处理后重新引发了新异常),异常就从发生异常的函数或方法逐渐向外传播,首先传给该函数或方法的调用者,该函数或方法的调用者再传给其调用者……直至最后传到Python解释器,此时Python解释器会中止该程序,并打印异常的传播轨迹信息。
python专门提供了traceback模块来处理异常传播轨迹,使用traceback可以方便地处理python的异常传播轨迹,traceback提供了两个常用方法:
- traceback.print_exc():将异常传播轨迹信息输出到控制台或指定文件中
- format_exc():将异常传播轨迹信息转换成字符串。
# 导入trackback模块
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 "C:Userszz.spyder-py3 emp.py", line 14, in <module>
main()
File "C:Userszz.spyder-py3 emp.py", line 6, in main
firstMethod()
File "C:Userszz.spyder-py3 emp.py", line 8, in firstMethod
secondMethod()
File "C:Userszz.spyder-py3 emp.py", line 10, in secondMethod
thirdMethod()
File "C:Userszz.spyder-py3 emp.py", line 12, in thirdMethod
raise SelfException("自定义异常信息")
SelfException: 自定义异常信息
异常处理原则
成功的异常处理应该实现如下4个目标。
- 使程序代码混乱最小化。
- 捕获并保留诊断信息。
- 通知合适的人员。
- 采用合适的方式结束异常活动。
不要过度使用异常
必须指出:异常处理机制的初衷是将不可预期异常的处理代码和正常的业务逻辑处理代码分离,因此绝不要使用异常处理来代替正常的业务逻辑判断。
不是使用过于庞大的try块
正确的做法是,把大块的try块分割成多个可能出现异常的程序段落,并把它们放在单独的try块中,从而分别捕获并处理异常。
不要忽略捕获到的异常
通常建议对异常采取适当措施,比如:
- 处理异常。对异常进行合适的修复,然后绕过异常发生的地方继续运行;或者用别的数据进行计算,以代替期望的方法返回值;或者提示用户重新操作……总之,程序应该尽量修复异常,使程序能恢复运行。
- 重新引发新异常。把在当前运行环境下能做的事情尽量做完,然后进行异常转译,把异常包装成当前层的异常,重新传给上层调用者。
- 在合适的层处理异常。如果当前层不清楚如何处理异常,就不要在当前层使用except语句来捕获该异常,让上层调用者来负责处理该异常。
原文来源于我的语雀,我的微信公众号:细细研磨