Python 課程 – 例外與錯誤處理(Exception Handling)
主題:try / except / else / finally
簡介
在撰寫任何實務應用程式時,錯誤是不可避免的。不論是使用者輸入不符合期待、檔案不存在,或是外部服務回傳異常,都可能讓程式在執行途中中斷。若程式直接拋出未捕獲的例外,使用者將看到一長串的 traceback,體驗極差,甚至可能導致資料遺失或系統不穩定。
Python 提供了結構化的例外處理機制,讓開發者能夠:
- 預先捕捉可能發生的錯誤,避免程式意外崩潰。
- 根據錯誤類型做不同的回應(例如重新嘗試、回報使用者、寫入日誌)。
- 確保資源(檔案、網路連線、資料庫)一定會被正確釋放,即使發生例外。
本篇文章將深入探討 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 中使用 return、break、raise,finally 仍會先被執行。
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,忘記釋放資源 |
可能導致檔案、連線等資源未關閉。 | 使用 finally 或 with 語句(上下文管理器)確保資源自動釋放。 |
在 try 內寫過多程式碼 |
若錯誤發生位置不明,難以判斷是哪一行拋出例外。 | 把可能拋例外的程式碼拆成小段,或使用多個 try/except。 |
在 except 中再次拋出不同例外,卻未保留原始資訊 |
失去原始錯誤的堆疊資訊,除錯困難。 | 使用 raise NewError(...) from e 來保留鏈接(exception chaining)。 |
忘記 else 的意義 |
把「成功」的程式碼寫在 try 後,會在例外發生時仍被執行。 |
把只有在成功時才需要的程式碼搬到 else,讓邏輯更清晰。 |
最佳實踐:
- 盡量使用
with(上下文管理器)代替手動try/finally釋放資源。with open('log.txt', 'a') as f: f.write('記錄訊息') - 保持例外層級清晰:業務層拋出自訂例外,底層拋出標準例外,讓呼叫者可以分層捕捉。
- 記錄日誌:在
except中使用logging模組寫入錯誤日誌,而不是僅僅print。 - 避免在
except中做大量運算:例外處理應該盡可能快,將耗時工作放在else或其他函式中。 - 測試例外路徑:使用單元測試(
unittest、pytest)模擬例外情境,確保程式在錯誤發生時仍能正確回復。
實際應用場景
Web API 輸入驗證
- 前端送來的 JSON 可能缺少欄位或型別不符,使用
try/except捕捉KeyError、TypeError,回傳 400 Bad Request。
- 前端送來的 JSON 可能缺少欄位或型別不符,使用
批次資料處理
- 讀取大量檔案時,個別檔案可能損壞或缺失。使用
try/except包住每一次讀檔,記錄失敗檔案,讓批次作業不中斷。
- 讀取大量檔案時,個別檔案可能損壞或缺失。使用
多執行緒或併發程式
- 每個執行緒的工作函式都需要自行捕捉例外,避免未捕捉的例外導致整個執行緒崩潰,並在
finally中釋放鎖或信號量。
- 每個執行緒的工作函式都需要自行捕捉例外,避免未捕捉的例外導致整個執行緒崩潰,並在
資料庫交易
- 如前面範例所示,使用
try/except/else/finally確保交易的 原子性(All‑Or‑Nothing),即使中途發生錯誤也能安全回滾。
- 如前面範例所示,使用
外部服務整合
- 呼叫第三方 API 時,可能遇到超時、授權失敗、服務不可用等情況。透過分層的
except捕捉不同錯誤,並在finally中關閉 Session,提升系統韌性。
- 呼叫第三方 API 時,可能遇到超時、授權失敗、服務不可用等情況。透過分層的
總結
try / except / else / finally是 Python 例外處理的核心組件,能讓程式在 錯誤發生時保持可控,同時確保資源的正確釋放。- 適當分層捕捉(先捕最具體,再捕較廣),搭配 自訂例外,可以讓錯誤訊息更具語意、除錯更快速。
else用於 成功路徑的後續處理,讓程式結構更清晰;finally則是 清理資源的最後保障。- 避免過度寬鬆的
except:、在except中忘記釋放資源、以及把過多程式碼塞進單一try,是常見的陷阱。 - 最佳實踐 包括使用
with、記錄日誌、保持例外層級清晰,以及在測試中覆蓋例外路徑。
掌握這套例外處理機制,你的 Python 程式將更具 健壯性、可維護性,也能在真實專案中有效降低意外崩潰的風險。祝你寫程式愉快,錯誤掌控自如!