Python – 例外與錯誤處理:raise 自訂例外
簡介
在日常開發中,**例外(Exception)**是傳遞錯誤資訊、讓程式在非預期情況下安全退出或恢復的重要機制。Python 本身提供了豐富的內建例外類別,但在大型專案或特定業務領域,僅靠內建例外往往無法清楚表達「發生了什麼錯誤」以及「該如何處理」。此時,我們可以 自行定義例外類別,搭配 raise 主動拋出,讓錯誤資訊更具語意、除錯成本更低。
本篇文章將帶你從 為什麼需要自訂例外、如何建立與拋出,到 實務上常見的陷阱與最佳實踐,一步步掌握在 Python 中使用 raise 產生自訂例外的技巧,適合剛入門的學員,也能為已有開發經驗的你提供實務參考。
核心概念
1️⃣ 為什麼要自訂例外?
- 語意明確:
ValueError、KeyError只能說「值錯誤」或「鍵不存在」,自訂例外可以直接說「使用者年齡不合法」。 - 層級分離:在大型系統中,錯誤可能分為「系統層」與「業務層」;自訂例外讓不同層級的錯誤可以分別捕獲。
- 統一管理:所有相關錯誤集中於一個模組,日後維護或擴充更方便。
2️⃣ 建立自訂例外類別的基本語法
在 Python 中,例外本質上是 繼承自 BaseException(或更常用的 Exception)的類別。最簡單的寫法如下:
class MyError(Exception):
"""自訂例外,用於示範最基本的寫法。"""
pass
小技巧:即使不加任何屬性,
pass也能讓類別完整成立,這是最常見的「標記例外」方式。
3️⃣ raise 的使用方式
- 拋出單一例外
raise MyError("發生了自訂錯誤") - 拋出已建立的例外實例(適合需要傳遞多個參數時)
err = MyError("錯誤代碼 1001", "資料格式不符") raise err
4️⃣ 帶參數的自訂例外
有時候我們想要把錯誤代碼、相關資料一起傳遞,這時可以在 __init__ 中自行定義屬性,並覆寫 __str__ 讓錯誤訊息更友好:
class ValidationError(Exception):
"""驗證失敗時拋出的例外,攜帶錯誤代碼與欄位資訊。"""
def __init__(self, field: str, message: str, code: int = 0):
self.field = field
self.message = message
self.code = code
super().__init__(message)
def __str__(self):
return f"[Error {self.code}] 欄位「{self.field}」驗證失敗:{self.message}"
使用範例:
def check_age(age: int):
if not (0 <= age <= 120):
raise ValidationError("age", "年齡必須介於 0~120 之間", code=1001)
# 呼叫
try:
check_age(-5)
except ValidationError as ve:
print(ve) # [Error 1001] 欄位「age」驗證失敗:年齡必須介於 0~120 之間
5️⃣ 例外鏈結(Exception Chaining)
在捕獲底層例外後,我們常會拋出更具業務意義的自訂例外,這時可以使用 raise ... from 讓兩個例外形成因果鏈結,方便除錯:
class DatabaseError(Exception):
"""資料庫操作失敗的自訂例外。"""
pass
def query_user(uid: int):
try:
# 假設這裡會拋出 KeyError
data = {"name": "Alice"}[uid]
return data
except KeyError as ke:
raise DatabaseError(f"查無使用者 ID {uid}") from ke
當 DatabaseError 被捕獲時,Python 會同時顯示原始的 KeyError 堆疊,讓開發者快速定位根本原因。
6️⃣ 在套件或模組中集中管理例外
對於較大的專案,建議在 exceptions.py(或類似名稱)中集中定義所有自訂例外,並使用 命名空間 讓外部程式碼易於引用:
# exceptions.py
class AppError(Exception):
"""所有自訂例外的基底類別,便於一次捕獲。"""
pass
class AuthError(AppError):
"""認證失敗時拋出。"""
pass
class PermissionError(AppError):
"""權限不足時拋出。"""
pass
在其他模組使用:
from .exceptions import AuthError, PermissionError
def login(user, pwd):
if not verify(user, pwd):
raise AuthError("帳號或密碼錯誤")
if not has_permission(user):
raise PermissionError("使用者權限不足")
程式碼範例(實用 5 篇)
範例 1:最簡單的自訂例外
class SimpleError(Exception):
"""示範最簡單的自訂例外。"""
pass
def do_something(flag: bool):
if not flag:
raise SimpleError("旗標必須為 True")
說明:只要
flag為False,即拋出SimpleError,呼叫端只需要except SimpleError即可捕獲。
範例 2:帶參數的驗證例外(前文 ValidationError)
class ValidationError(Exception):
def __init__(self, field, message, code=0):
self.field = field
self.message = message
self.code = code
super().__init__(message)
def __str__(self):
return f"[Error {self.code}] {self.field}: {self.message}"
def validate_email(email: str):
if "@" not in email:
raise ValidationError("email", "必須包含 @ 符號", code=2002)
try:
validate_email("invalid_email")
except ValidationError as ve:
print(ve) # [Error 2002] email: 必須包含 @ 符號
範例 3:例外鏈結(raise ... from)
class ServiceError(Exception):
"""外部服務呼叫失敗的例外。"""
pass
def call_external_api():
try:
# 模擬底層的 TimeoutError
raise TimeoutError("連線逾時")
except TimeoutError as te:
raise ServiceError("呼叫第三方 API 失敗") from te
try:
call_external_api()
except ServiceError as se:
print(se)
# 會同時顯示 TimeoutError 的追蹤資訊
範例 4:在套件中統一管理例外
# mypkg/exceptions.py
class MyPkgError(Exception):
"""所有自訂例外的根基類別。"""
pass
class ConfigError(MyPkgError):
"""設定檔錯誤。"""
pass
class NetworkError(MyPkgError):
"""網路相關錯誤。"""
pass
# mypkg/config.py
from .exceptions import ConfigError
def load_config(path: str):
if not path.endswith(".json"):
raise ConfigError(f"不支援的設定檔格式:{path}")
# app.py
from mypkg.config import load_config
from mypkg.exceptions import MyPkgError
try:
load_config("settings.yaml")
except MyPkgError as e:
print(f"設定載入失敗:{e}")
範例 5:結合 Logging 的自訂例外
import logging
logger = logging.getLogger(__name__)
class DataProcessingError(Exception):
"""資料處理過程中的錯誤。"""
def __init__(self, step, detail):
self.step = step
self.detail = detail
super().__init__(detail)
logger.error("Step %s failed: %s", step, detail)
def process_data(data):
if not isinstance(data, dict):
raise DataProcessingError("parse", "資料必須是 dict")
# 其他處理...
重點:在例外的
__init__中直接寫入日誌,確保每次拋出例外都會留下紀錄,對於線上服務的除錯非常有幫助。
常見陷阱與最佳實踐
| 常見陷阱 | 為什麼會發生 | 最佳實踐 |
|---|---|---|
濫用基礎 Exception |
直接拋出 Exception 會讓呼叫端無法分辨錯誤類型。 |
建立 專屬基底類別(如 AppError),所有自訂例外都繼承自它。 |
捕獲過寬的例外 (except Exception as e) |
可能把程式碼錯誤(如 NameError)也吞掉,隱藏 bug。 |
只捕獲 預期的自訂例外,或使用 except (SpecificError1, SpecificError2) as e。 |
在 __init__ 中做大量運算 |
例外被拋出時會執行不必要的程式,降低效能。 | __init__ 只負責 儲存資訊,任何耗時操作請放在拋出前完成。 |
| 忘記加入 docstring | 其他開發者不清楚例外的使用情境。 | 為每個自訂例外撰寫 簡潔說明,並說明何時拋出、需要哪些參數。 |
| 例外名稱不具語意 | 讀者無法快速了解錯誤來源。 | 使用 動詞 + 名詞(如 AuthError、PermissionDenied)或 領域前綴(如 DBConnectionError)。 |
| 未使用例外鏈結 | 失去原始錯誤堆疊,除錯困難。 | 在捕獲底層例外後,用 raise NewError(...) from e 保留因果關係。 |
| 在 except 區塊內再次拋出同樣例外 | 可能造成無限迴圈或重複日誌。 | 若要重新拋出,使用 raise(不加例外類別),讓原始例外保持原始堆疊。 |
實際應用場景
API 輸入驗證
- 前端送來的 JSON 需要嚴格檢查,若欄位缺失或格式錯誤,拋出
RequestValidationError,讓 API 中間層統一回傳 400 錯誤碼與說明。
- 前端送來的 JSON 需要嚴格檢查,若欄位缺失或格式錯誤,拋出
資料庫交易失敗
- 在多表寫入時,任何一步失敗都拋出
TransactionError,外層捕獲後自動rollback,確保資料一致性。
- 在多表寫入時,任何一步失敗都拋出
第三方服務整合
- 呼叫支付平台、郵件服務等外部 API 時,若回傳非 2xx 狀態碼,拋出
ExternalServiceError,並使用raise ... from保存原始HTTPError。
- 呼叫支付平台、郵件服務等外部 API 時,若回傳非 2xx 狀態碼,拋出
業務規則違反
- 例如「同一使用者同一天只能領取一次優惠券」,違反時拋出
CouponLimitError,讓上層 UI 能直接顯示友善訊息。
- 例如「同一使用者同一天只能領取一次優惠券」,違反時拋出
批次處理與 ETL
- 每一步資料清理或轉換失敗時拋出
DataPipelineError,在主程式捕獲後記錄失敗的檔案名稱、步驟與原因,方便後續人工修正。
- 每一步資料清理或轉換失敗時拋出
總結
- 自訂例外 讓錯誤資訊更具語意、層級分明,對於大型或長期維護的專案尤為重要。
- 建立 專屬基底類別、使用
raise ... from以及 加入日誌,都是提升除錯效率的關鍵技巧。 - 謹記 不要濫用
Exception、避免過寬捕獲,以及 為例外寫清楚的 docstring,才能讓程式碼保持可讀、可維護。 - 透過本文的 5 個實用範例,你已掌握從最簡單的標記例外到帶參數、鏈結、集中管理與日誌整合的完整流程。
把這些概念套用到實際的開發情境中,你的程式將不再因為「不知道是哪裡出錯」而卡住,而是能 快速定位、清晰回報、優雅恢復,讓 Python 應用程式的穩定性與可維護性大幅提升。祝你寫程式愉快,錯誤處理更無懼!