Python 自訂例外 (Custom Exceptions)

你可以建立自己的例外類別,讓錯誤處理更有語意且更容易管理。

建立自訂例外

繼承 Exception 類別:

class MyError(Exception):
    pass

# 使用
raise MyError("Something went wrong")

加入自訂屬性

class ValidationError(Exception):
    def __init__(self, field, message):
        self.field = field
        self.message = message
        super().__init__(f"{field}: {message}")

try:
    raise ValidationError("email", "Invalid email format")
except ValidationError as e:
    print(f"Field: {e.field}")
    print(f"Message: {e.message}")

輸出:

Field: email
Message: Invalid email format

建立例外階層

# 基礎例外
class AppError(Exception):
    """應用程式的基礎例外"""
    pass

# 資料庫相關例外
class DatabaseError(AppError):
    """資料庫相關錯誤"""
    pass

class ConnectionError(DatabaseError):
    """資料庫連線錯誤"""
    pass

class QueryError(DatabaseError):
    """查詢錯誤"""
    pass

# 驗證相關例外
class ValidationError(AppError):
    """驗證錯誤"""
    pass

class RequiredFieldError(ValidationError):
    """必填欄位錯誤"""
    pass

class InvalidFormatError(ValidationError):
    """格式錯誤"""
    pass

使用例外階層可以靈活地捕捉例外:

try:
    # 可能拋出各種例外的程式碼
    pass
except ConnectionError:
    # 只處理連線錯誤
    pass
except DatabaseError:
    # 處理所有資料庫錯誤(包括 ConnectionError 和 QueryError)
    pass
except AppError:
    # 處理所有應用程式錯誤
    pass

完整的自訂例外類別

class APIError(Exception):
    """API 相關錯誤的基礎類別"""
    
    def __init__(self, message, status_code=None, details=None):
        super().__init__(message)
        self.message = message
        self.status_code = status_code
        self.details = details or {}
    
    def to_dict(self):
        return {
            "error": self.__class__.__name__,
            "message": self.message,
            "status_code": self.status_code,
            "details": self.details
        }

class NotFoundError(APIError):
    def __init__(self, resource, resource_id):
        message = f"{resource} with id {resource_id} not found"
        super().__init__(message, status_code=404)
        self.resource = resource
        self.resource_id = resource_id

class AuthenticationError(APIError):
    def __init__(self, message="Authentication failed"):
        super().__init__(message, status_code=401)

class PermissionError(APIError):
    def __init__(self, message="Permission denied"):
        super().__init__(message, status_code=403)

# 使用
try:
    user_id = 123
    user = find_user(user_id)
    if not user:
        raise NotFoundError("User", user_id)
except NotFoundError as e:
    print(e.to_dict())

實際應用範例

表單驗證

class FormValidationError(Exception):
    def __init__(self):
        super().__init__("Form validation failed")
        self.errors = {}
    
    def add_error(self, field, message):
        if field not in self.errors:
            self.errors[field] = []
        self.errors[field].append(message)
    
    def has_errors(self):
        return len(self.errors) > 0
    
    def __str__(self):
        error_messages = []
        for field, messages in self.errors.items():
            for msg in messages:
                error_messages.append(f"{field}: {msg}")
        return "\n".join(error_messages)

def validate_user_form(data):
    errors = FormValidationError()
    
    if not data.get("username"):
        errors.add_error("username", "Username is required")
    elif len(data["username"]) < 3:
        errors.add_error("username", "Username must be at least 3 characters")
    
    if not data.get("email"):
        errors.add_error("email", "Email is required")
    elif "@" not in data["email"]:
        errors.add_error("email", "Invalid email format")
    
    if not data.get("password"):
        errors.add_error("password", "Password is required")
    elif len(data["password"]) < 8:
        errors.add_error("password", "Password must be at least 8 characters")
    
    if errors.has_errors():
        raise errors
    
    return True

# 使用
try:
    validate_user_form({
        "username": "ab",
        "email": "invalid-email",
        "password": "123"
    })
except FormValidationError as e:
    print("Validation errors:")
    print(e)

業務邏輯例外

class InsufficientFundsError(Exception):
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        super().__init__(
            f"Insufficient funds: balance={balance}, required={amount}"
        )

class AccountLockedError(Exception):
    def __init__(self, account_id, reason):
        self.account_id = account_id
        self.reason = reason
        super().__init__(f"Account {account_id} is locked: {reason}")

class BankAccount:
    def __init__(self, account_id, balance=0):
        self.account_id = account_id
        self.balance = balance
        self.locked = False
        self.lock_reason = None
    
    def withdraw(self, amount):
        if self.locked:
            raise AccountLockedError(self.account_id, self.lock_reason)
        if amount > self.balance:
            raise InsufficientFundsError(self.balance, amount)
        self.balance -= amount
        return self.balance
    
    def lock(self, reason):
        self.locked = True
        self.lock_reason = reason

# 使用
account = BankAccount("A001", 1000)

try:
    account.withdraw(1500)
except InsufficientFundsError as e:
    print(f"Cannot withdraw: {e}")
    print(f"Current balance: {e.balance}")
    print(f"Requested amount: {e.amount}")

Context Manager 中的例外

class TransactionError(Exception):
    pass

class Transaction:
    def __init__(self, name):
        self.name = name
    
    def __enter__(self):
        print(f"Starting transaction: {self.name}")
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None:
            print(f"Rolling back transaction: {self.name}")
            print(f"Error: {exc_val}")
            # 回傳 True 表示例外已處理
            # 回傳 False 表示讓例外繼續傳播
            return False
        print(f"Committing transaction: {self.name}")
        return True
    
    def execute(self, query):
        print(f"Executing: {query}")
        if "error" in query:
            raise TransactionError("Query failed")

# 使用
with Transaction("update_user") as t:
    t.execute("UPDATE users SET name='Alice'")
    t.execute("This will cause error")  # 會觸發回滾

最佳實踐

  1. 繼承 Exception 而不是 BaseException
  2. 建立有意義的例外階層
  3. 包含有用的錯誤資訊
  4. 保持例外類別簡單
  5. 為相關的例外建立基礎類別
# 好的例外設計
class OrderError(Exception):
    """訂單相關錯誤的基礎類別"""
    pass

class OrderNotFoundError(OrderError):
    def __init__(self, order_id):
        self.order_id = order_id
        super().__init__(f"Order {order_id} not found")

class InvalidOrderStatusError(OrderError):
    def __init__(self, order_id, current_status, required_status):
        self.order_id = order_id
        self.current_status = current_status
        self.required_status = required_status
        super().__init__(
            f"Order {order_id} has status '{current_status}', "
            f"but '{required_status}' is required"
        )