本文 AI 產出,尚未審核

Python 課程 – 例外與錯誤處理(Exception Handling)

主題:多重例外類型


簡介

在日常開發中,程式不可能永遠「一帆風順」;檔案不存在、使用者輸入錯誤、網路斷線等情況都會導致程式拋出例外 (Exception)。如果只用單一的 except: 來捕捉所有例外,雖然可以避免程式直接崩潰,但失去瞭解問題根本原因的機會,也容易掩蓋隱藏的錯誤。

多重例外類型 讓我們能針對不同的錯誤類別分別處理,提供更精確的回饋、資源釋放或補救措施。對於初學者而言,掌握這項技巧不僅能寫出更穩定的程式,也能培養良好的除錯與設計習慣,對日後進階開發(如網路服務、資料庫操作)更是必備基礎。


核心概念

1. 例外類別的層級結構

Python 內建的例外類別都繼承自 BaseException,其中最常用的是 Exception。以下是一段簡化的繼承圖:

BaseException
 └─ Exception
     ├─ ArithmeticError
     │   ├─ ZeroDivisionError
     │   └─ OverflowError
     ├─ LookupError
     │   ├─ IndexError
     │   └─ KeyError
     ├─ ValueError
     └─ OSError
         ├─ FileNotFoundError
         └─ PermissionError

了解這個階層可以幫助我們選擇最適當的捕捉層級

  • 若只想捕捉「所有」例外,使用 except Exception:
  • 若只想處理「檔案相關」的錯誤,使用 except OSError: 或更具體的 FileNotFoundError

2. 同時捕捉多個例外

Python 允許在同一個 except 子句中列出多個例外類型,只要用圓括號把它們包起來即可:

try:
    # 可能拋出 ZeroDivisionError 或 ValueError
    result = int(input("請輸入除數: ")) / int(input("請輸入被除數: "))
except (ZeroDivisionError, ValueError) as e:
    print(f"輸入錯誤或除以零:{e}")

此寫法的好處是程式碼更簡潔,且在處理「同類型」的回應時不需要重複寫 except 區塊。

3. 分別處理不同例外

有時候不同的錯誤需要不同的補救措施,這時就要分段寫多個 except

try:
    with open("data.txt", "r") as f:
        data = f.read()
        number = int(data)
except FileNotFoundError:
    print("檔案不存在,請確認路徑是否正確。")
except ValueError:
    print("檔案內容不是有效的整數,請檢查檔案內容。")
except Exception as e:
    # 捕捉其他未預期的例外
    print(f"發生未知錯誤:{e}")

這樣即使 FileNotFoundErrorValueError 同時可能發生,我們仍能提供最貼切的錯誤訊息與處理方式。

4. elsefinally 的搭配

  • else:只有在 try 區塊 沒有拋出例外 時才會執行,適合放置「成功後的後續工作」。
  • finally:不論是否拋出例外,都會執行,常用於釋放資源(關閉檔案、釋放鎖等)。
try:
    conn = db.connect()
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users")
    rows = cursor.fetchall()
except db.DatabaseError as e:
    print(f"資料庫錯誤:{e}")
else:
    print(f"成功取得 {len(rows)} 筆資料")
finally:
    # 確保連線一定會被關閉
    if conn:
        conn.close()

5. 自訂例外類別

在大型專案中,我們常會定義自己的例外類別,以表示業務層面的錯誤。自訂例外必須繼承自 Exception

class InsufficientBalanceError(Exception):
    """帳戶餘額不足的例外"""
    def __init__(self, balance, amount):
        super().__init__(f"餘額 {balance} 不足以扣除 {amount}")
        self.balance = balance
        self.amount = amount

def withdraw(account, amount):
    if account['balance'] < amount:
        raise InsufficientBalanceError(account['balance'], amount)
    account['balance'] -= amount

# 使用範例
try:
    withdraw({'balance': 500}, 800)
except InsufficientBalanceError as e:
    print(e)        # => 餘額 500 不足以扣除 800

自訂例外讓錯誤的意圖更清晰,也方便在上層捕捉時只針對特定業務錯誤處理。


程式碼範例

以下提供 5 個實用範例,涵蓋從基礎到稍微進階的多重例外處理方式。每段程式碼皆加上說明性註解,方便讀者快速理解。

範例 1:同時捕捉兩種常見錯誤

def safe_division():
    """取得使用者輸入並執行除法,捕捉 ZeroDivisionError 與 ValueError"""
    try:
        a = int(input("請輸入被除數: "))
        b = int(input("請輸入除數: "))
        return a / b
    except (ZeroDivisionError, ValueError) as err:
        # 同時處理除以零與非整數輸入的情況
        print(f"錯誤:{err}")
        return None

result = safe_division()
if result is not None:
    print(f"結果 = {result}")

重點:使用 (ZeroDivisionError, ValueError) 只寫一次 except,讓程式碼更簡潔。

範例 2:分別處理檔案與資料轉換錯誤

def read_number_from_file(path):
    """讀取檔案內容並轉換成整數,針對不同例外提供不同訊息"""
    try:
        with open(path, "r") as f:
            content = f.read().strip()
            return int(content)
    except FileNotFoundError:
        print(f"找不到檔案:{path}")
    except ValueError:
        print(f"檔案內容不是整數:{content}")
    except Exception as e:
        # 捕捉其他未預期的錯誤
        print(f"未知錯誤:{e}")
    return None

num = read_number_from_file("data.txt")
if num is not None:
    print(f"讀取的數字是 {num}")

技巧except Exception 放在最後,避免遮蔽前面的特定例外。

範例 3:使用 elsefinally 處理資料庫連線

import sqlite3

def query_user(user_id):
    conn = None
    try:
        conn = sqlite3.connect("example.db")
        cur = conn.cursor()
        cur.execute("SELECT name FROM users WHERE id=?", (user_id,))
        row = cur.fetchone()
        if row is None:
            raise LookupError(f"找不到 ID 為 {user_id} 的使用者")
    except sqlite3.Error as db_err:
        print(f"資料庫錯誤:{db_err}")
    except LookupError as lk_err:
        print(lk_err)
    else:
        print(f"使用者名稱:{row[0]}")
    finally:
        if conn:
            conn.close()          # 確保連線一定被釋放
            print("資料庫連線已關閉")

query_user(3)

說明else 只在查詢成功且未拋例外時執行,finally 負責資源釋放。

範例 4:自訂例外與多層捕捉

class InvalidConfigurationError(Exception):
    """設定檔錯誤的自訂例外"""

def load_config(path):
    try:
        with open(path, "r") as f:
            cfg = f.read()
            if not cfg.startswith("{"):
                raise InvalidConfigurationError("設定檔格式不正確")
            return cfg
    except FileNotFoundError:
        raise InvalidConfigurationError(f"找不到設定檔:{path}")

def start_service():
    try:
        cfg = load_config("config.json")
        print("服務已啟動")
    except InvalidConfigurationError as ic_err:
        print(f"啟動失敗:{ic_err}")

start_service()

關鍵:在 load_config 中把所有與設定檔相關的錯誤 統一拋出 InvalidConfigurationError,讓上層只需要捕捉一個例外即可。

範例 5:使用 try … except … else 進行網路請求

import requests

def fetch_json(url):
    """取得 JSON 資料,分別處理連線錯誤與 JSON 解析錯誤"""
    try:
        response = requests.get(url, timeout=5)
        response.raise_for_status()          # 若 HTTP 狀態碼非 2xx 會拋出 HTTPError
    except requests.exceptions.HTTPError as http_err:
        print(f"HTTP 錯誤:{http_err}")
    except requests.exceptions.ConnectionError as conn_err:
        print(f"連線失敗:{conn_err}")
    except requests.exceptions.Timeout:
        print("請求逾時")
    else:
        try:
            return response.json()
        except ValueError:
            print("回傳內容不是合法的 JSON")
    return None

data = fetch_json("https://api.github.com")
if data:
    print("成功取得資料:", data.get("current_user_url"))

重點:先捕捉網路層面的例外,成功取得回應後再處理 JSON 解析,使用 else 區分不同階段的錯誤。


常見陷阱與最佳實踐

常見陷阱 為何會發生 建議的做法
使用 except: 捕捉所有例外 會把 KeyboardInterruptSystemExit 之類的系統例外也捕捉,導致程式無法正常中斷或退出。 只捕捉 Exception 或更具體的例外類型;若真的需要捕捉所有,最後再加上 except BaseException: 作特殊處理。
把太多例外寫在同一個 except 失去針對不同錯誤提供客製化訊息的機會,除錯時資訊不足。 盡量 分段寫 except,只在處理方式完全相同時才合併。
except 裡直接 returnraise,卻忘記釋放資源 可能導致檔案、資料庫連線等資源泄漏。 使用 finallywith 語句確保資源釋放。
自訂例外沒有繼承自 Exception 會讓 except Exception: 捕捉不到,導致例外傳遞過遠。 自訂例外必須繼承 Exception,且建議在 __init__ 中呼叫 super().__init__
捕捉過寬的例外(如 OSError 會把不相關的錯誤一起捕捉,掩蓋真正的問題。 根據需求選擇最小的例外類型,如只想處理檔案不存在則捕捉 FileNotFoundError

最佳實踐清單

  1. 先想好要捕捉哪些例外,再寫 try…except,避免「把所有錯誤都抓起來」的慣性。
  2. 盡量使用 with(上下文管理器)處理檔案、鎖、資料庫連線等需要自動釋放的資源。
  3. 在需要時使用自訂例外,讓錯誤的語意更明確。
  4. 保持例外訊息的可讀性:在拋出或記錄例外時,加入足夠的上下文資訊(如檔案路徑、使用者 ID)。
  5. 不要在 except 裡沉默失敗(除非真的確定可以安全忽略),至少要記錄或打印訊息,以免日後排錯困難。

實際應用場景

場景 需要的多重例外處理 範例說明
檔案批次處理 FileNotFoundErrorPermissionErrorUnicodeDecodeError 讀取大量文字檔時,若檔案缺失、權限不足或編碼錯誤,都要分別回報並繼續處理其他檔案。
使用者輸入驗證 ValueErrorTypeError、自訂 InvalidInputError 把輸入的字串轉型時,可能拋出 ValueError;若類型不符合需求則拋自訂例外。
網路服務呼叫 requests.exceptions.TimeoutHTTPErrorJSONDecodeError 在呼叫外部 API 時,分別處理連線逾時、HTTP 錯誤、回傳資料非 JSON。
資料庫交易 sqlite3.IntegrityErrorsqlite3.OperationalError、自訂 BusinessRuleError 插入資料時可能違反唯一鍵(IntegrityError),或因資料庫鎖定失敗(OperationalError),亦可能因業務規則不符拋自訂例外。
多執行緒/多程序同步 RuntimeError(如鎖已釋放)、TimeoutError、自訂 DeadlockError 在併發環境中,必須捕捉同步相關的錯誤,以避免程式卡死或資源競爭。

透過上表,我們可以看到 每個實務情境都有其特定的錯誤類型,而使用多重例外正是讓程式在面對這些不同情況時仍能保持可讀、可維護的關鍵。


總結

  • 多重例外類型 是 Python 錯誤處理的核心技巧,能讓我們針對不同失敗原因提供恰當的回應。
  • 熟悉例外類別的階層、使用 except (A, B) 合併相同處理、以及分段捕捉不同例外,是寫出健全、易除錯程式的基本功。
  • 搭配 elsefinallywith 以及 自訂例外,可以進一步提升程式的可讀性與資源安全性。
  • 在實務開發中,務必先思考要捕捉哪些例外,避免過度寬鬆或過度嚴格,並遵循最佳實踐(如記錄錯誤、釋放資源)。

掌握了這些概念與技巧,你就能在日常開發、資料處理、網路服務等各種情境下,寫出更可靠、更具可維護性的 Python 程式。祝你在程式之路上除錯少一點、開發多一點! 🚀