本文 AI 產出,尚未審核

Python – 例外與錯誤處理:raise 自訂例外


簡介

在日常開發中,**例外(Exception)**是傳遞錯誤資訊、讓程式在非預期情況下安全退出或恢復的重要機制。Python 本身提供了豐富的內建例外類別,但在大型專案或特定業務領域,僅靠內建例外往往無法清楚表達「發生了什麼錯誤」以及「該如何處理」。此時,我們可以 自行定義例外類別,搭配 raise 主動拋出,讓錯誤資訊更具語意、除錯成本更低。

本篇文章將帶你從 為什麼需要自訂例外如何建立與拋出,到 實務上常見的陷阱與最佳實踐,一步步掌握在 Python 中使用 raise 產生自訂例外的技巧,適合剛入門的學員,也能為已有開發經驗的你提供實務參考。


核心概念

1️⃣ 為什麼要自訂例外?

  • 語意明確ValueErrorKeyError 只能說「值錯誤」或「鍵不存在」,自訂例外可以直接說「使用者年齡不合法」。
  • 層級分離:在大型系統中,錯誤可能分為「系統層」與「業務層」;自訂例外讓不同層級的錯誤可以分別捕獲。
  • 統一管理:所有相關錯誤集中於一個模組,日後維護或擴充更方便。

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")

說明:只要 flagFalse,即拋出 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 其他開發者不清楚例外的使用情境。 為每個自訂例外撰寫 簡潔說明,並說明何時拋出、需要哪些參數。
例外名稱不具語意 讀者無法快速了解錯誤來源。 使用 動詞 + 名詞(如 AuthErrorPermissionDenied)或 領域前綴(如 DBConnectionError)。
未使用例外鏈結 失去原始錯誤堆疊,除錯困難。 在捕獲底層例外後,用 raise NewError(...) from e 保留因果關係
在 except 區塊內再次拋出同樣例外 可能造成無限迴圈或重複日誌。 若要重新拋出,使用 raise(不加例外類別),讓原始例外保持原始堆疊。

實際應用場景

  1. API 輸入驗證

    • 前端送來的 JSON 需要嚴格檢查,若欄位缺失或格式錯誤,拋出 RequestValidationError,讓 API 中間層統一回傳 400 錯誤碼與說明。
  2. 資料庫交易失敗

    • 在多表寫入時,任何一步失敗都拋出 TransactionError,外層捕獲後自動 rollback,確保資料一致性。
  3. 第三方服務整合

    • 呼叫支付平台、郵件服務等外部 API 時,若回傳非 2xx 狀態碼,拋出 ExternalServiceError,並使用 raise ... from 保存原始 HTTPError
  4. 業務規則違反

    • 例如「同一使用者同一天只能領取一次優惠券」,違反時拋出 CouponLimitError,讓上層 UI 能直接顯示友善訊息。
  5. 批次處理與 ETL

    • 每一步資料清理或轉換失敗時拋出 DataPipelineError,在主程式捕獲後記錄失敗的檔案名稱、步驟與原因,方便後續人工修正。

總結

  • 自訂例外 讓錯誤資訊更具語意、層級分明,對於大型或長期維護的專案尤為重要。
  • 建立 專屬基底類別使用 raise ... from 以及 加入日誌,都是提升除錯效率的關鍵技巧。
  • 謹記 不要濫用 Exception避免過寬捕獲,以及 為例外寫清楚的 docstring,才能讓程式碼保持可讀、可維護。
  • 透過本文的 5 個實用範例,你已掌握從最簡單的標記例外到帶參數、鏈結、集中管理與日誌整合的完整流程。

把這些概念套用到實際的開發情境中,你的程式將不再因為「不知道是哪裡出錯」而卡住,而是能 快速定位、清晰回報、優雅恢復,讓 Python 應用程式的穩定性與可維護性大幅提升。祝你寫程式愉快,錯誤處理更無懼!