2.26. 引發錯誤¶
函式可以透過引發(raising)一個例外來向其呼叫端發出問題的訊號。關鍵字是 raise:
def square_root(x):
if x < 0:
raise ValueError("square_root expects a non-negative number")
return x ** 0.5
呼叫 square_root(-1) 會停在 raise 那一行,跳出函式,並在呼叫端尋找相符的 except。如果沒有任何呼叫端攔截它,指令碼就會以一段追溯訊息(traceback)結束。
2.26.1. 為什麼要引發例外而不是回傳哨兵值¶
回報「不良輸入」有兩種方式:
# signal with a sentinel
def square_root_or_none(x):
if x < 0:
return None
return x ** 0.5
# raise an exception
def square_root(x):
if x < 0:
raise ValueError("...")
return x ** 0.5
例外的形式通常比較好:
呼叫端必須刻意地處理錯誤情況 -- 要嘛用
try,要嘛讓例外向外傳播。哨兵值容易被忘記,也容易被誤認為正常結果。錯誤訊息會隨著例外一起傳遞;哨兵值的做法則必須把診斷資訊附加在其他地方。
未處理例外的預設行為是一次明顯的崩潰,並附帶一段指向出問題呼叫的追溯訊息。靜默地回傳
None之後會變成難以察覺的錯誤。
只有當「找不到」是一種例行且非例外性的結果時,才使用哨兵值 -- dict.get() 在鍵缺失時回傳 None,正是因為查詢本來就預期有時會落空。
2.26.2. 自訂例外類別¶
若要引發一個呼叫端可能想要與內建錯誤區分開來的問題,請定義一個 Exception 的子類別:
class ConfigError(Exception):
pass
def load_config(path):
try:
f = open(path)
except OSError as e:
raise ConfigError("missing config file: " + path)
try:
load_config("settings.json")
except ConfigError as e:
print("startup failed:", e)
空的 class 主體沒有問題 -- 重要的是名稱本身,因為呼叫端是依類別來攔截的。如果呼叫端可能想在同一個區塊中攔截整個系列的錯誤,請把相關的錯誤歸在一個共同的基底之下。
2.26.2.1. 重新引發¶
在 except 區塊內使用裸 raise 會重新引發目前的例外,使其向上傳播到下一個處理器:
try:
do_work()
except Exception as e:
log(e)
raise # let it keep going
當函式想要觀察某個錯誤(記錄它、計數它、復原部分變更)而不實際處理它時,這就是正確的形式。
2.26.3. 何時攔截、何時傳播¶
一個實用的經驗法則:
在能夠有意義地復原的層級攔截例外 -- 替換成預設值、重試、略過不良輸入。
當除了崩潰之外無計可施,或當上層才是知道如何復原的那一層時,就讓它向外傳播。
位於呼叫堆疊中段、會吞掉所有錯誤並靜默回傳的函式,會讓失敗無從追溯。請優先讓例外一路傳遞,直到抵達真正有應對計畫的程式碼。