Python 例外處理(Exception Handling)讓程式在遇到錯誤時不直接崩潰,而是優雅地捕捉錯誤、執行補救措施並繼續運作。本文完整解析 try、except、else、finally 四個子句的語法與執行順序、常見內建例外類型、raise 主動拋出例外、自訂例外類別,以及量化策略中資料讀取與 API 呼叫的防崩潰實戰設計。
什麼是例外處理?
程式執行時難免遇到意外狀況——使用者輸入非數字、檔案不存在、網路連線中斷、除以零……這些都會觸發 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 包裹,搭配重試機制,讓策略在網路波動時能自動恢復而不是直接掛掉。