Python 例外處理:try、except、finally 與 raise

Python 例外處理(Exception Handling)讓程式在遇到錯誤時不直接崩潰,而是優雅地捕捉錯誤、執行補救措施並繼續運作。本文完整解析 tryexceptelsefinally 四個子句的語法與執行順序、常見內建例外類型、raise 主動拋出例外、自訂例外類別,以及量化策略中資料讀取與 API 呼叫的防崩潰實戰設計。

Cinematic dark alchemy forge with a glowing protective arcane barrier catching and containing a dangerous explosion of red error sparks inside a stone containment chamber, a mystical safety mechanism absorbing the failure while the surrounding forge continues operating, deep dramatic shadows, cinematic lighting, 16:9

什麼是例外處理?

程式執行時難免遇到意外狀況——使用者輸入非數字、檔案不存在、網路連線中斷、除以零……這些都會觸發 Python 的例外(Exception),若不加以處理,程式會立即中止並印出錯誤訊息。例外處理語法讓你能夠預先設置捕捉網,攔截特定錯誤並決定如何應對,讓程式健壯地繼續運作。

可以把例外處理想像成煉金坊的安全隔離艙:正常煉製流程在 try 艙室中進行;若發生爆炸(例外),except 隔離艙自動啟動吸收衝擊;無論是否爆炸,finally 的善後程序都會執行(如關閉爐門、清理現場);只有安全煉製完成時,else 的後續步驟才會啟動。

核心語法:try、except、else、finally

# === 基本 try-except ===

# 沒有例外處理的程式(遇到錯誤直接崩潰)
# print(10 / 0)   # ZeroDivisionError: division by zero(程式中止)

# ✅ 用 try-except 捕捉例外
try:
    result = 10 / 0          # 這行會觸發 ZeroDivisionError
    print(result)            # 這行不會執行
except ZeroDivisionError:
    print("錯誤:不能除以零") # 捕捉到例外,執行這裡

print("程式繼續執行")        # 例外被捕捉後,程式正常繼續

# 輸出:
# 錯誤:不能除以零
# 程式繼續執行
try 區塊內放「可能出錯的程式碼」;except 後面指定要捕捉的例外類型,若 try 內發生該類型的錯誤,就執行 except 區塊。try 區塊中觸發例外後,該行之後的程式碼會被跳過,直接進入 except 區塊。
# === 取得例外物件資訊(as 語法)===
try:
    value = int("abc")           # ValueError:無法把 "abc" 轉成整數
except ValueError as e:
    print(f"發生 ValueError:{e}")
    # 發生 ValueError:invalid literal for int() with base 10: 'abc'

# === 捕捉多種例外類型 ===
def safe_divide(a, b):
    try:
        result = a / b
        return result
    except ZeroDivisionError:
        print("錯誤:除數不能為零")
        return None
    except TypeError as e:
        print(f"錯誤:型別不對 → {e}")
        return None

print(safe_divide(10, 2))    # 5.0
print(safe_divide(10, 0))    # 錯誤:除數不能為零 → None
print(safe_divide(10, "a"))  # 錯誤:型別不對 → None

# 一個 except 同時捕捉多種例外(用 Tuple)
try:
    val = int(input("請輸入數字:"))
except (ValueError, TypeError) as e:
    print(f"輸入有誤:{e}")
as e 可以把例外物件賦值給變數 e,透過 str(e) 或直接 print(e) 取得錯誤的詳細說明,對除錯與日誌記錄非常有用。建議永遠指定具體的例外類型,不要裸寫 except:(捕捉所有例外),否則會把程式設計錯誤也一起吃掉,讓 bug 難以發現。
# === else 與 finally 子句 ===

def read_price(filename):
    try:
        f = open(filename, 'r')        # 嘗試開啟檔案
        data = f.read()
    except FileNotFoundError:
        print(f"檔案不存在:{filename}")
        return None
    else:
        # 只有 try 完全成功(沒有例外)才執行
        print("檔案讀取成功")
        return data
    finally:
        # 無論是否發生例外,一定執行(通常用來釋放資源)
        print("執行清理:關閉相關資源")
        # f.close()  ← 若 open 成功,這裡應該關閉檔案

# 執行順序:
# ① try 區塊
# ② 若無例外 → else 區塊 → finally 區塊
# ③ 若有例外 → except 區塊 → finally 區塊
# finally 永遠執行,無論如何

規則與注意事項

子句 執行時機 常見用途
try 永遠執行(放可能出錯的程式碼) 包裹風險操作:IO、型別轉換、網路、除法
except try 發生指定例外時執行 錯誤補救、印出警告、回傳預設值
else try 完全成功(無例外)才執行 成功後的後續邏輯,與 except 互斥
finally 無論是否發生例外,永遠執行 釋放資源(關閉檔案、關閉連線、清理暫存)

常見錯誤與防呆

錯誤一:裸寫 except: 吃掉所有例外(包括 bug)

# ❌ 危險寫法(連程式設計錯誤也被吃掉,bug 難以發現)
try:
    result = my_undefined_function()   # NameError:函數未定義
except:
    print("發生錯誤")   # 被靜默吞掉,看不出是什麼錯

# ✅ 正確做法:明確指定例外類型
try:
    result = int("abc")
except ValueError as e:
    print(f"ValueError:{e}")

# 若真的需要捕捉所有例外,至少用 Exception(不含系統例外)
try:
    risky_operation()
except Exception as e:
    print(f"未預期的例外:{type(e).__name__}: {e}")
    raise    # 記錄後重新拋出,不要靜默吞掉

錯誤二:在 finally 中 return,覆蓋 try 的回傳值

# ❌ 陷阱:finally 中的 return 會覆蓋 try 的 return
def risky():
    try:
        return "成功"
    finally:
        return "finally"   # 這會覆蓋上面的 "成功"!

print(risky())   # "finally"(不是 "成功")

# ✅ finally 只做清理,不要有 return
def risky():
    try:
        return "成功"
    finally:
        print("清理資源")   # 只執行副作用,不 return

錯誤三:except 例外順序錯誤(子類別被父類別攔截)

# ❌ 錯誤順序(Exception 是所有例外的父類別,會攔截所有例外)
try:
    result = 10 / 0
except Exception:
    print("捕捉到 Exception")    # ZeroDivisionError 也被這裡攔截
except ZeroDivisionError:
    print("捕捉到除以零")        # 永遠不會執行到這裡!

# ✅ 正確順序:子類別(具體)寫前面,父類別(廣泛)寫後面
try:
    result = 10 / 0
except ZeroDivisionError:
    print("捕捉到除以零")        # 先捕捉具體例外
except Exception as e:
    print(f"其他例外:{e}")      # 再捕捉廣泛例外

進階用法

raise:主動拋出例外

raise 讓你能夠主動觸發例外,用於輸入驗證、前置條件檢查,以及在 except 區塊中記錄錯誤後重新拋出,讓上層呼叫者知道發生了問題。

# raise 主動拋出內建例外
def set_period(period):
    if not isinstance(period, int) or period <= 0:
        raise ValueError(f"period 必須是正整數,收到:{period}")
    return period

try:
    set_period(-5)
except ValueError as e:
    print(f"參數錯誤:{e}")
# 參數錯誤:period 必須是正整數,收到:-5

# raise 不帶引數:在 except 內重新拋出當前例外
def load_data(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError as e:
        print(f"[LOG] 找不到檔案:{path}")
        raise    # 記錄後重新拋出,讓呼叫者知道失敗了

自訂例外類別

繼承 Exception 可以建立專屬的例外類型,讓錯誤語意更清晰,在大型量化系統中有助於分層處理不同來源的錯誤。

# 自訂例外類別(繼承 Exception)
class InvalidPriceError(Exception):
    """價格資料不合法時拋出"""
    pass

class InsufficientDataError(Exception):
    """歷史資料筆數不足時拋出"""
    def __init__(self, required, actual):
        super().__init__(f"需要 {required} 筆,實際只有 {actual} 筆")
        self.required = required
        self.actual   = actual

# 使用自訂例外
def validate_closes(closes, min_bars=20):
    for c in closes:
        if c is None or c <= 0:
            raise InvalidPriceError(f"無效的收盤價:{c}")
    if len(closes) < min_bars:
        raise InsufficientDataError(min_bars, len(closes))
    return True

try:
    validate_closes([100, 105, -1, 110], min_bars=20)
except InvalidPriceError as e:
    print(f"價格錯誤:{e}")      # 價格錯誤:無效的收盤價:-1
except InsufficientDataError as e:
    print(f"資料不足:{e}")

量化實戰:防崩潰的資料載入與 API 呼叫

量化系統中最常見的失敗點是外部資料存取,用 try-except-finally 包裹每個 IO 操作,搭配重試機制,讓策略在資料異常時能優雅降級而非直接崩潰。

import time

def fetch_price_with_retry(ticker, max_retries=3, delay=1.0):
    """帶重試機制的價格獲取函數"""
    for attempt in range(1, max_retries + 1):
        try:
            # 模擬 API 呼叫(實際使用時替換為真實 API)
            if attempt < 3:
                raise ConnectionError("模擬網路逾時")
            price = 185.2    # 第三次成功
            return price

        except ConnectionError as e:
            print(f"  第 {attempt} 次嘗試失敗:{e}")
            if attempt < max_retries:
                print(f"  {delay} 秒後重試...")
                time.sleep(delay)
            else:
                print(f"  已達最大重試次數,放棄")
                return None

        except Exception as e:
            print(f"  未預期錯誤:{type(e).__name__}: {e}")
            return None

        finally:
            print(f"  [第 {attempt} 次嘗試結束]")

price = fetch_price_with_retry("AAPL")
print(f"最終價格:{price}")
# 第 1 次嘗試失敗:模擬網路逾時
# [第 1 次嘗試結束]
# 1.0 秒後重試...
# 第 2 次嘗試失敗:模擬網路逾時
# [第 2 次嘗試結束]
# 1.0 秒後重試...
# [第 3 次嘗試結束]
# 最終價格:185.2

煉金坊小叮嚀

例外處理是讓程式從「能跑」進化到「穩定運行」的關鍵技巧。在量化系統中,一個未被捕捉的例外可能讓整個回測或實盤策略在關鍵時刻崩潰,後果非常嚴重。鐵則一:永遠不要裸寫 except:,至少寫 except Exception as e: 並把錯誤資訊記錄下來——靜默吞掉例外是製造隱藏 bug 的最快方法。鐵則二:finally 只做資源釋放,絕對不要在裡面寫 return,否則會把 try 的回傳值覆蓋掉,這個 bug 極其隱蔽。在量化開發中,我對所有外部資料存取(API、CSV 讀取、資料庫查詢)都加上 try-except 包裹,搭配重試機制,讓策略在網路波動時能自動恢復而不是直接掛掉。

張貼留言

較新的 較舊