现实世界里不完美的意外和异常会在不经意间发生,从而使生活不得不暂时偏离正常轨道,软件世界也是如此。
或因为外部原因,或因为内部原因,程序会在某些条件下产生异常或者错误。
为了提高系统的健壮性和用户的友好性,需要一定的机制来处理这种情况。
跟其他很多编程语言一样,Python也提供了异常处理机制。
Python中常用的异常处理语法是try
、except
、else
、finally
,它们可以有多种组合,
如try-except
、try-except-else
、try-finally
以及try-except-else-finally
等。
异常处理的流程
比较全面的组合 try-except-dse-finally
异常处理的流程,语法形式如下:
try:
<statements> # Run the main action first
except <name1>:
<statements> # 当try中发生name1的异常时处理
except <name2, name3>:
<statements> # 当try中发生name2或name3中的某一个异常的时候处理
except <name4> as <data>:
<statements> # 当try中发生name4的异常时处理,获取对应实例
except:
<statements> # 其他异常发生时处理
else:
<statements> # 没有异常发生时执行
finally:
<statements> # 不管有没有异常都会执行
最为全面的组合 try-except-dse-finally
异常处理的流程如图所示:
异常处理通常需要遵循以下几点基本原则
- 注意异常的粒度,不推荐在
try
中放入过多的代码。 - 谨慎使用单独的
except
语句处理所有异常,最好能定位具体的异常。 - 注意异常捕获的顺序,在合适的层次处理异常。
异常的粒度是人为划分的,在处理异常的时候最好保持异常粒度的一致性和合理性,
同时要避免在 try
中放入过多的代码,即避免异常粒度过大。
在 try
中放入过多的代码带来的问题是如果程序中抛出异常,将会较难定位,
给debug和修复带来不便,因此应尽量只在可能抛出异常的语句块前卤放入 try
语句。
同样也不推荐使用 except Exception
或者 except StandardError
来捕获异常。
从表面上看,在 try
后面单独使用 except
语句可以捕获所有的异常似乎是个不错的做法,
但实际上会带来什么问题呢?来看以下简单的例子:
# import sys
# try:
# print( a)
# b=0
# print (a/b)
# except:
# sys.exit("ZeroDivisionError:Can not division zero")
An exception has occurred, use %tb to see the full traceback. SystemExit: ZeroDivisionError:Can not division zero
/opt/conda/lib/python3.12/site-packages/IPython/core/interactiveshell.py:3587: UserWarning: To exit: use 'exit', 'quit', or Ctrl-D. warn("To exit: use 'exit', 'quit', or Ctrl-D.", stacklevel=1)
程序运行以打印结束,这会以为是发生了除数为零的错误,
但实际情况是因为a
在使用前并没有定义,程序引发了 NameError
。
而对于单独的 except
语句的使用,真实的错误往往被掩盖。
对上述代码修改如下:
# import sys
# try:
# print( a ,b =0)
# print (a/b)
# except ZeroDivisionError:
# sys.exit("ZeroDivisionError:Can not division zero")
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[3], line 3 1 import sys 2 try: ----> 3 print( a ,b =0) 4 print (a/b) 5 except ZeroDivisionError: NameError: name 'a' is not defined
运行程序输出 NameError
异常如上面结果。
单独使用 except
会捕获包括 SystemExit
, Keyboard
, nterrupt
等在内的各种异常,
从而掩盖程序真正发生异常的原因,给debug造成一定的迷惑性。
因此需要谨慎使用,最好能在 except
语句中定位具体的异常。
如果在某些情况下不得不使用单独的 except
语句,最好能够使用 raise
语句将异常抛出向上层传递。
Python中内建异常以类的形式出现,
Python2.5后异常被迁移到新式类上,启用一个新的所有异常之母的 BaseException
类,
内建异常有一定的继承结构,如 UnicodeDecodeError
继承自 UnicodeError
,
而其继承链结构为 UnicodeDecodeError —>UnicodcError —>ValueError -->Exception ->BaseEJtception,如图 3-2 所示。
用户也可以继承自内建异常构建自己的异常类,从而在内建类的继承结构上进一步延伸,在这种情况下异常捕获的顺序显得非常重要。
为了精确地定位错误发生的原因,推荐的方法是将继承结构中子类异常在前面的 except
语句中抛出,
而父类异常在后面的 except
语句拋出。这样做的原因是当 try
块中有异常发生的时候,
解铎器根据 except
声明的顺序进行匹配,在第一个匹配的地方便立即处理该异常。
如果将5次较高的异常类在前面进行捕获,往往不能精确地定位异常发生的具体位置。
如下例中 ValueError
声明在前而 UnicodeDecodeError
在后,当拋出 UnicodeDecodeError
异常的时候,
由于它是 ValueError
的子类,在 ValueError
处便直接被捕获了,
打印出消息“ ValueError occured ”,而真正的异常 UnicodeDecodeError
却被悄然掩盖。
图3-2 UnkodeDecodeError
继承结构示意阁
try:
raise UnicodeDecodeError('pdfdocencoding', "a",2,-1, "not support decoding")
except ValueError:
#Value£rror 为 UnicodeDecocieError 的父类,捕获异常时却在前面
print( "ValueError occured")
except UnicodeDecodeError as e:
print(e)
因此,异常捕获的顺序非常重要,同时异常应该在适当的位被处理,
一个原则就是如果异常能够在被捕获的位置被处理,那么应该及时处理,
不能处理也应该以合适的方式向上层抛出。遇到异常不论好歹就向上层抛出是非常不明智的。
向上层传递的时候需要瞀惕异常被丢失的情况,可以使用不带参数的 raise
来传递。
try:
somecode()
except:
revert_stuff ()
raise
使用更为友好的异常信息,遵守异常参数的规范。软件最终是为用户服务的,异常发生的时候, 异常信息清晰友好与否直接关系到用户体验。通常来说有两类异常阅读者: 使用软件的人和开发软件的人,即用户和开发者。对于用户来说关注更多的是业务。 先来看一段异常信息:
Traceback (most recent call last):
File ntest.py", line 4f in <module>
print ItemPriceTable['a']
KeyError: 'a'
如果将这段信息给一个没有软件编程背景的人看,他肯定会觉得如读天书一般,
对于用户这并不是一种较为友好的做法,在面对用户的时候异常信息应该以较为清晰和明确的方式显示出来,
如下例中当査找一个不在列表的水果价格的时候给出相关的提示信息会比直接抛出 KeyError
信息要友好得多。
import sys
import traceback
ItemPriceTable ={ "apple":'3,5' ,'orangem':' 4 ' ,'cheery':'20','mango':'8' }
def getprice(itemname):
try:
price = ItemPriceTable[itemname]
return price
except KeyError:
print ("%s can not find in the price table^you should input another Jtind of fruit"%sys.exc_value) #屋示异常相关的提示信息
while 1:
itemname = input("Enter the fruit get the price or press x to exit: ")
if itemname == "x":
break
price = getprice(itemname)
if price != None:
print ("%s's price is $%s/kg" % (itemname, price))
此外,如果内建异常类不能满足需求,用户可以在继承内建异常的基础上针对特定的业务逻辑定义自己的异常类。 但无论是内建异常类,还是用户定义的异常类,在传递异常参数的时候都需要遵守异常参数规范。