能否手動拋出一個異常嗎?
答案是肯定的,Python允許程序自行引發異常,使用 raise 語句即可。
異常是一種很“主觀”的說法,以下雨為例,假設大家約好明天去爬山郊游,如果第二天下雨了,這種情況會打破既定計划,就屬於一種異常;但對於正在期盼天降甘霖的農民而言,如果第二天下雨了,他們正好隨雨追肥,這就完全正常。
很多時候,系統是否要引發異常,可能需要根據應用的業務需求來決定,如果程序中的數據、執行與既定的業務需求不符,這就是一種異常。由於與業務需求不符而產生的異常,必須由程序員來決定引發,系統無法引發這種異常。
如果需要在程序中自行引發異常,則應使用 raise 語句,該語句的基本語法格式為:
raise [exceptionName [(reason)]]
其中,用 [] 括起來的為可選參數,其作用是指定拋出的異常名稱,以及異常信息的相關描述。如果可選參數全部省略,則 raise 會把當前錯誤原樣拋出;如果僅省略 (reason),則在拋出異常時,將不附帶任何的異常描述信息。
也就是說,raise 語句有如下三種常用的用法:
- raise:單獨一個 raise。該語句引發當前上下文中捕獲的異常(比如在 except 塊中),或默認引發 RuntimeError 異常。
- raise 異常類名稱:raise 后帶一個異常類名稱。該語句引發指定異常類的默認實例。
- raise 異常類名稱(描述信息):在引發指定異常的同時,附帶異常的描述信息。
上面三種用法最終都是要引發一個異常實例(即使指定的是異常類,實際上也是引發該類的默認實例),raise 語句每次只能引發一個異常實例。
例如:
>>> raise
Traceback (most recent call last):
File "<pyshell#1>", line 1, in
raise
RuntimeError: No active exception to reraise
>>> raise ZeroDivisionError
Traceback (most recent call last):
File "<pyshell#0>", line 1, in
raise ZeroDivisionError
ZeroDivisionError
>>> raise ZeroDivisionError("除數不能為零")
Traceback (most recent call last):
File "<pyshell#2>", line 1, in
raise ZeroDivisionError("除數不能為零")
ZeroDivisionError: 除數不能為零
當然,我們手動讓程序引發異常,很多時候並不是為了讓其崩潰。事實上,raise 語句引發的異常通常用 try except(else finally)異常處理結構來捕獲並進行處理。例如:
try:
a = input("輸入一個數:")
#判斷用戶輸入的是否為數字
if(not a.isdigit()):
raise ValueError("a 必須是數字")
except ValueError as e:
print("引發異常:",repr(e))
程序運行結果為:
輸入一個數:a
引發異常: ValueError('a 必須是數字',)
可以看到,當用戶輸入的不是數字時,程序會進入 if 判斷語句,並執行 raise 引發 ValueError 異常。但由於其位於 try 塊中,因為 raise 拋出的異常會被 try 捕獲,並由 except 塊進行處理。
因此,雖然程序中使用了 raise 語句引發異常,但程序的執行是正常的,手動拋出的異常並不會導致程序崩潰。
raise 不需要參數
正如前面所看到的,在使用 raise 語句時可以不帶參數,例如:
try:
a = input("輸入一個數:")
if(not a.isdigit()):
raise ValueError("a 必須是數字")
except ValueError as e:
print("引發異常:",repr(e))
raise
程序執行結果為:
輸入一個數:a
引發異常: ValueError('a 必須是數字',)
Traceback (most recent call last):
File "D:\python3.6\1.py", line 4, in
raise ValueError("a 必須是數字")
ValueError: a 必須是數字
這里重點關注位於 except 塊中的 raise,由於在其之前我們已經手動引發了 ValueError 異常,因此這里當再使用 raise 語句時,它會再次引發一次。
當在沒有引發過異常的程序使用無參的 raise 語句時,它默認引發的是 RuntimeError 異常。例如:
try:
a = input("輸入一個數:")
if(not a.isdigit()):
raise
except RuntimeError as e:
print("引發異常:",repr(e))
程序執行結果為:
輸入一個數:a
引發異常: RuntimeError('No active exception to reraise',)
except 和 raise 同時使用
在實際應用中對異常可能需要更復雜的處理方式。當一個異常出現時,單靠某個方法無法完全處理該異常,必須由幾個方法協作才可完全處理該異常。也就是說,在異常出現的當前方法中,程序只對異常進行部分處理,還有些處理需要在該方法的調用者中才能完成,所以應該再次引發異常,讓該方法的調用者也能捕獲到異常。
為了實現這種通過多個方法協作處理同一個異常的情形,可以在 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()
上面程序中 9~13 行代碼對應的 except 塊捕獲到異常后,系統打印了該異常的字符串信息,接着引發一個 AuctionException 異常,通知該方法的調用者再次處理該 AuctionException 異常。所以程序中的 main() 函數,也就是 bid() 方法的調用者還可以再次捕獲 AuctionException 異常,井將該異常的詳細描述信息打印出來。
這種 except 和 raise 結合使用的情況在實際應用中非常常用。實際應用對異常的處理通常分成兩個部分:
- 應用后台需要通過日志來記錄異常發生的詳細情況;
- 應用還需要根據異常向應用使用者傳達某種提示;
在這種情形下,所有異常都需要兩個方法共同完成,也就必須將 except 和 raise 結合使用。
如果程序需要將原始異常的詳細信息直接傳播出去,Python 也允許用自定義異常對原始異常進行包裝,只要將上面 ① 號代碼改為如下形式即可:
raise AuctionException(e)
上面就是把原始異常 e 包裝成了 AuctionException 異常,這種方式也被稱為異常包裝或異常轉譯。
自定義異常類
很多時候,程序可選擇引發自定義異常,因為異常的類名通常也包含了該異常的有用信息。所以在引發異常時,應該選擇合適的異常類,從而可以明確地描述該異常情況。在這種情形下,應用程序常常需要引發自定義異常。
用戶自定義異常都應該繼承 Exception 基類或 Exception 的子類,在自定義異常類時基本不需要書寫更多的代碼,只要指定自定義異常類的父類即可。
下面程序創建了一個自定義異常類:
class AuctionException(Exception):
pass
上面程序創建了 AuctionException 異常類,該異常類不需要類體定義,因此使用 pass 語句作為占位符即可。
在大部分情況下,創建自定義異常類都可采用與上面程序相似的代碼來完成,只需改變 AuctionException 異常的類名即可,讓該異常的類名可以准確地描述該異常。