本文 AI 產出,尚未審核

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

主題:try / except / else / finally


簡介

在撰寫任何實務應用程式時,錯誤是不可避免的。不論是使用者輸入不符合期待、檔案不存在,或是外部服務回傳異常,都可能讓程式在執行途中中斷。若程式直接拋出未捕獲的例外,使用者將看到一長串的 traceback,體驗極差,甚至可能導致資料遺失或系統不穩定。

Python 提供了結構化的例外處理機制,讓開發者能夠:

  1. 預先捕捉可能發生的錯誤,避免程式意外崩潰。
  2. 根據錯誤類型做不同的回應(例如重新嘗試、回報使用者、寫入日誌)。
  3. 確保資源(檔案、網路連線、資料庫)一定會被正確釋放,即使發生例外。

本篇文章將深入探討 try / except / else / finally 四個關鍵關鍵字的使用方式,並提供實用範例、常見陷阱與最佳實踐,幫助你在日常開發中寫出更安全、可讀性更高的 Python 程式。


核心概念

1. try 块:執行可能拋出例外的程式碼

try 區塊內的程式碼會被 Python 監控,一旦發生例外,執行流程會立即跳到相對應的 except 區塊;若沒有例外,則會依序執行 else(如果有)以及 finally(如果有)。

try:
    # 可能會拋出例外的程式碼
    result = 10 / divisor
except ZeroDivisionError:
    # 處理除以零的情況
    print("除數不可為 0")

2. except:捕捉與處理例外

  • 單一例外類型:只捕捉特定的錯誤,避免把其他錯誤吞掉。
  • 多個例外類型:使用元組或多個 except,分別對不同錯誤做不同處理。
  • 取得例外資訊:使用 as e 取得例外物件,可取得錯誤訊息或自訂屬性。
try:
    value = int(user_input)
except (ValueError, TypeError) as e:
    # 捕捉多種轉型錯誤
    print(f"輸入錯誤:{e}")

3. else:在沒有例外時執行的程式碼

else 區塊只會在 try 中的程式碼順利執行完畢且未拋出例外時才會被呼叫。它常用於 與例外無關的後續處理,讓程式結構更清晰。

try:
    data = open('config.json')
except FileNotFoundError:
    print("設定檔不存在")
else:
    # 只有檔案成功開啟才會執行
    config = json.load(data)
    print("設定載入完成")

4. finally:無論是否發生例外,都會執行的清理工作

finally 區塊最常用於 釋放資源(關閉檔案、斷開連線、釋放鎖)或 寫入日誌。即使在 except 中使用 returnbreakraisefinally 仍會先被執行。

try:
    conn = sqlite3.connect('example.db')
    cursor = conn.cursor()
    cursor.execute('SELECT * FROM users')
    rows = cursor.fetchall()
except sqlite3.DatabaseError as e:
    print(f"資料庫錯誤:{e}")
finally:
    # 確保連線一定會關閉
    conn.close()
    print("資料庫連線已關閉")

程式碼範例彙整

以下提供 五個實務上常見 的例外處理範例,涵蓋不同情境與技巧。

範例 1:使用者輸入驗證與重試機制

def get_positive_int(prompt: str) -> int:
    """持續要求使用者輸入正整數,直到成功為止。"""
    while True:
        try:
            value = int(input(prompt))
            if value <= 0:
                raise ValueError("必須是正數")
        except ValueError as e:
            print(f"輸入錯誤:{e},請再試一次。")
        else:
            # 成功取得合法值
            return value

age = get_positive_int("請輸入您的年齡:")
print(f"您的年齡是 {age} 歲")

重點:利用 raise 主動拋出自訂例外,並在 except 中顯示友善訊息,else 區塊確保只有合法值會被回傳。

範例 2:檔案操作的完整流程(else + finally

import json

def load_config(path: str) -> dict:
    try:
        f = open(path, 'r', encoding='utf-8')
    except FileNotFoundError:
        print(f"找不到設定檔:{path}")
        return {}
    else:
        # 只有檔案成功開啟才會執行
        config = json.load(f)
        print("設定檔載入成功")
        return config
    finally:
        # 無論是否成功,都要關閉檔案
        try:
            f.close()
        except UnboundLocalError:
            # 若檔案根本沒開,就不需要關閉
            pass

技巧finally 中的 try/except 防止在檔案未開啟時呼叫 close() 產生 UnboundLocalError

範例 3:網路請求的例外分層處理

import requests

def fetch_json(url: str) -> dict | None:
    try:
        response = requests.get(url, timeout=5)
        response.raise_for_status()          # 產生 HTTPError(4xx/5xx)時拋出例外
    except requests.Timeout:
        print("連線逾時,請稍後再試")
    except requests.HTTPError as e:
        print(f"伺服器回傳錯誤:{e.response.status_code}")
    except requests.RequestException as e:
        # 捕捉其他所有 requests 相關的錯誤
        print(f"網路請求失敗:{e}")
    else:
        # 只有成功取得且狀態碼為 200 時才會執行
        return response.json()
    finally:
        # 若有需要可在此釋放資源,例如關閉 Session
        pass

data = fetch_json('https://api.example.com/data')
if data:
    print("取得資料:", data)

重點:先捕捉最具體的例外(Timeout),再捕捉較廣的例外(RequestException),避免「吞掉」更重要的錯誤資訊。

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

class ValidationError(Exception):
    """自訂驗證失敗例外,用於表單或資料驗證。"""
    pass

def validate_user(username: str, age: int) -> None:
    if not username.isalnum():
        raise ValidationError("使用者名稱只能包含英數字")
    if age < 0 or age > 120:
        raise ValidationError("年齡必須介於 0 到 120 之間")

try:
    validate_user("John_Doe!", -5)
except ValidationError as ve:
    print(f"驗證失敗:{ve}")
except Exception as e:
    print(f"其他錯誤:{e}")
else:
    print("使用者資料驗證通過")

技巧:自訂例外讓錯誤語意更清晰,且在多層捕捉時能更精準地分辨業務錯誤與程式錯誤。

範例 5:交易(Transaction)中的 finally 確保提交或回滾

import sqlite3

def transfer_funds(src: str, dst: str, amount: float) -> None:
    conn = sqlite3.connect('bank.db')
    try:
        cur = conn.cursor()
        # 扣款
        cur.execute("UPDATE accounts SET balance = balance - ? WHERE name = ?", (amount, src))
        # 入帳
        cur.execute("UPDATE accounts SET balance = balance + ? WHERE name = ?", (amount, dst))
        # 人為觸發錯誤測試
        # raise RuntimeError("模擬交易失敗")
    except Exception as e:
        conn.rollback()          # 失敗時回滾
        print(f"交易失敗:{e}")
    else:
        conn.commit()            # 成功時提交
        print("交易完成")
    finally:
        conn.close()
        print("資料庫連線已關閉")

transfer_funds('Alice', 'Bob', 100.0)

要點finally 確保資料庫連線一定會被關閉;else 用於提交交易,保持 ACID 的一致性。


常見陷阱與最佳實踐

陷阱 說明 解決方案
過度寬鬆的 except: 捕捉所有例外會隱藏程式錯誤,難以除錯。 只捕捉需要處理的特定例外類型,或在最後加一個 except Exception as e: 作為「最後防線」並記錄日誌。
except 中直接 return,忘記釋放資源 可能導致檔案、連線等資源未關閉。 使用 finallywith 語句(上下文管理器)確保資源自動釋放。
try 內寫過多程式碼 若錯誤發生位置不明,難以判斷是哪一行拋出例外。 把可能拋例外的程式碼拆成小段,或使用多個 try/except
except 中再次拋出不同例外,卻未保留原始資訊 失去原始錯誤的堆疊資訊,除錯困難。 使用 raise NewError(...) from e 來保留鏈接(exception chaining)。
忘記 else 的意義 把「成功」的程式碼寫在 try 後,會在例外發生時仍被執行。 把只有在成功時才需要的程式碼搬到 else,讓邏輯更清晰。

最佳實踐

  1. 盡量使用 with(上下文管理器)代替手動 try/finally 釋放資源。
    with open('log.txt', 'a') as f:
        f.write('記錄訊息')
    
  2. 保持例外層級清晰:業務層拋出自訂例外,底層拋出標準例外,讓呼叫者可以分層捕捉。
  3. 記錄日誌:在 except 中使用 logging 模組寫入錯誤日誌,而不是僅僅 print
  4. 避免在 except 中做大量運算:例外處理應該盡可能快,將耗時工作放在 else 或其他函式中。
  5. 測試例外路徑:使用單元測試(unittestpytest)模擬例外情境,確保程式在錯誤發生時仍能正確回復。

實際應用場景

  1. Web API 輸入驗證

    • 前端送來的 JSON 可能缺少欄位或型別不符,使用 try/except 捕捉 KeyErrorTypeError,回傳 400 Bad Request。
  2. 批次資料處理

    • 讀取大量檔案時,個別檔案可能損壞或缺失。使用 try/except 包住每一次讀檔,記錄失敗檔案,讓批次作業不中斷。
  3. 多執行緒或併發程式

    • 每個執行緒的工作函式都需要自行捕捉例外,避免未捕捉的例外導致整個執行緒崩潰,並在 finally 中釋放鎖或信號量。
  4. 資料庫交易

    • 如前面範例所示,使用 try/except/else/finally 確保交易的 原子性(All‑Or‑Nothing),即使中途發生錯誤也能安全回滾。
  5. 外部服務整合

    • 呼叫第三方 API 時,可能遇到超時、授權失敗、服務不可用等情況。透過分層的 except 捕捉不同錯誤,並在 finally 中關閉 Session,提升系統韌性。

總結

  • try / except / else / finally 是 Python 例外處理的核心組件,能讓程式在 錯誤發生時保持可控,同時確保資源的正確釋放。
  • 適當分層捕捉(先捕最具體,再捕較廣),搭配 自訂例外,可以讓錯誤訊息更具語意、除錯更快速。
  • else 用於 成功路徑的後續處理,讓程式結構更清晰;finally 則是 清理資源的最後保障
  • 避免過度寬鬆的 except:、在 except 中忘記釋放資源、以及把過多程式碼塞進單一 try,是常見的陷阱。
  • 最佳實踐 包括使用 with、記錄日誌、保持例外層級清晰,以及在測試中覆蓋例外路徑。

掌握這套例外處理機制,你的 Python 程式將更具 健壯性可維護性,也能在真實專案中有效降低意外崩潰的風險。祝你寫程式愉快,錯誤掌控自如!