前言

5. 例外處理 介紹的try…except被動地捕捉系統產生的錯誤。但有些情況「語法上完全正確、程式也跑得動」,只是不符合我們的業務規則 —— 例如速度變成負數、年齡填了 200 歲。

這種錯誤 Python 不會幫我們發現,必須主動拋出例外來提醒。這就是raise的用途。

raise 的語法

raise [例外類型 [(e)]]
  • 例外類型:可以是系統內建的例外類型(例如Exception),也可以是自己定義的例外類型。
  • e:是自己提供的參數,可以傳遞例外的資訊。

拋出系統內建的例外

[簡例] 高速公路設定速限為 70~110,如果速度過快或太慢,就主動拋出例外,提醒用路人注意安全。(檔名:raise1.py

[結果]

現在速度:60,速度太慢了!
目前時速:100
現在速度:150,已經超速了!

說明

  1. 1~5行:自訂函式檢查速度是否太慢或超速。
  2. 2~3行:如果速度太慢拋出Exception例外,並傳遞「速度太慢了!」訊息。
  3. 4~5行:如果速度太快拋出Exception例外,並傳遞「已經超速了!」訊息。
  4. 9行:呼叫函式檢查速度。
  5. 10~11行:接收Exception的例外,並以參數e顯示接收的訊息。
  6. 13行:如果速度正常(沒有拋出例外),else區塊顯示目前時速。

raise 會立刻中斷函式

一旦執行到raise,函式立即結束,後面的程式不會執行 —— 效果類似return,但它是「異常結束」,會被try…except接住。

這也是為什麼上例中速度 150 時,第一個if speed < 70不成立、第二個成立就拋出,函式直接結束。

拋出自訂的例外

除了內建的例外類型,我們也可以定義自己的例外類別。書上的寫法是繼承RuntimeError類別:

class MyException(RuntimeError):
    def __init__(self, arg):
        self.args = arg

[簡例] 同上例,如果速度過快或太慢,就主動拋出例外;如果速度正常,就以自訂例外拋出「快樂駕駛,平安返家!」訊息。(檔名:raise2.py

[結果]

現在速度:60,速度太慢了!
目前時速:100,快樂駕駛,平安返家!
現在速度:150,已經超速了!

說明

  1. 1~3行:自訂MyException繼承RuntimeError類別,建構式接收參數arg
  2. 10~11行:速度正常會主動拋出自訂的MyException例外。
  3. 13~15行:自訂函式將 tuple 轉換為字串。
  4. 20~22行:接收MyException的例外,因為e.args是 tuple 型別,以convertTuple將 tuple 轉換為字串。

except 的順序很重要

MyException繼承自RuntimeError,而RuntimeError又屬於Exception。因此**except MyException必須寫在except Exception前面** —— 否則所有例外都會先被Exception接走,自訂例外的區塊永遠不會執行。

通則:越specific(子類別)的例外要寫在越前面,Exception一定放最後。 這與 5. 例外處理 提到的原則一致。

⚠️ 關於書上寫法的兩點補充

一、self.args = arg 會把字串拆成一個個字元

上面的範例為什麼需要convertTuple()把 tuple 接回字串?這其實是self.args = arg造成的副作用。

args是 Python 例外類別的特殊屬性,它規定內容必須是 tuple。當你指派一個字串給它時,Python 會把字串逐字元拆開:

實測結果:

class MyException(RuntimeError):
    def __init__(self, arg):
        self.args = arg
 
raise MyException("快樂駕駛,平安返家!")
# e.args → ('快', '樂', '駕', '駛', ',', '平', '安', '返', '家', '!')

所以才需要用''.join(e.args)把它「黏」回去。這是自己製造出來、又自己解決的問題。

二、標準寫法不需要覆寫 __init__

Python 的例外類別本來就會處理訊息,直接繼承就好:

class MyException(Exception):     # 不用寫 __init__
    pass
 
try:
    raise MyException("快樂駕駛,平安返家!")
except MyException as e:
    print(e)                      # 直接印出:快樂駕駛,平安返家!

實測結果:

e.args = ('快樂駕駛,平安返家!',)     ← 完整字串,而且已經是 tuple
str(e) = '快樂駕駛,平安返家!'
書上寫法建議寫法
需要寫__init__❌ 不用
需要convertTuple()❌ 不用
取得訊息''.join(e.args)str(e) 或直接 print(e)

三、繼承 RuntimeError 不是必要的

書上說「定義的類別必須繼承RuntimeError類別」,但實際上繼承Exception即可,這也是 Python 官方建議的做法。

class MyException(Exception):     # ✅ 標準做法
    pass

RuntimeError只是Exception眾多子類別中的一個,語意上是指「執行時期的錯誤」。除非你的例外真的屬於這個語意,否則直接繼承Exception更恰當。

什麼時候該自訂例外

當你希望呼叫者能夠分別處理不同的錯誤時就該自訂。
例如一個提款程式,可以定義餘額不足密碼錯誤兩種例外,讓呼叫端各自給不同的提示訊息 —— 這比回傳False或錯誤代碼清楚得多。


相關主題與延伸閱讀