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}")
這樣即使 FileNotFoundError 與 ValueError 同時可能發生,我們仍能提供最貼切的錯誤訊息與處理方式。
4. else 與 finally 的搭配
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:使用 else 與 finally 處理資料庫連線
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: 捕捉所有例外 |
會把 KeyboardInterrupt、SystemExit 之類的系統例外也捕捉,導致程式無法正常中斷或退出。 |
只捕捉 Exception 或更具體的例外類型;若真的需要捕捉所有,最後再加上 except BaseException: 作特殊處理。 |
把太多例外寫在同一個 except |
失去針對不同錯誤提供客製化訊息的機會,除錯時資訊不足。 | 盡量 分段寫 except,只在處理方式完全相同時才合併。 |
在 except 裡直接 return 或 raise,卻忘記釋放資源 |
可能導致檔案、資料庫連線等資源泄漏。 | 使用 finally 或 with 語句確保資源釋放。 |
自訂例外沒有繼承自 Exception |
會讓 except Exception: 捕捉不到,導致例外傳遞過遠。 |
自訂例外必須繼承 Exception,且建議在 __init__ 中呼叫 super().__init__。 |
捕捉過寬的例外(如 OSError) |
會把不相關的錯誤一起捕捉,掩蓋真正的問題。 | 根據需求選擇最小的例外類型,如只想處理檔案不存在則捕捉 FileNotFoundError。 |
最佳實踐清單
- 先想好要捕捉哪些例外,再寫
try…except,避免「把所有錯誤都抓起來」的慣性。 - 盡量使用
with(上下文管理器)處理檔案、鎖、資料庫連線等需要自動釋放的資源。 - 在需要時使用自訂例外,讓錯誤的語意更明確。
- 保持例外訊息的可讀性:在拋出或記錄例外時,加入足夠的上下文資訊(如檔案路徑、使用者 ID)。
- 不要在
except裡沉默失敗(除非真的確定可以安全忽略),至少要記錄或打印訊息,以免日後排錯困難。
實際應用場景
| 場景 | 需要的多重例外處理 | 範例說明 |
|---|---|---|
| 檔案批次處理 | FileNotFoundError、PermissionError、UnicodeDecodeError |
讀取大量文字檔時,若檔案缺失、權限不足或編碼錯誤,都要分別回報並繼續處理其他檔案。 |
| 使用者輸入驗證 | ValueError、TypeError、自訂 InvalidInputError |
把輸入的字串轉型時,可能拋出 ValueError;若類型不符合需求則拋自訂例外。 |
| 網路服務呼叫 | requests.exceptions.Timeout、HTTPError、JSONDecodeError |
在呼叫外部 API 時,分別處理連線逾時、HTTP 錯誤、回傳資料非 JSON。 |
| 資料庫交易 | sqlite3.IntegrityError、sqlite3.OperationalError、自訂 BusinessRuleError |
插入資料時可能違反唯一鍵(IntegrityError),或因資料庫鎖定失敗(OperationalError),亦可能因業務規則不符拋自訂例外。 |
| 多執行緒/多程序同步 | RuntimeError(如鎖已釋放)、TimeoutError、自訂 DeadlockError |
在併發環境中,必須捕捉同步相關的錯誤,以避免程式卡死或資源競爭。 |
透過上表,我們可以看到 每個實務情境都有其特定的錯誤類型,而使用多重例外正是讓程式在面對這些不同情況時仍能保持可讀、可維護的關鍵。
總結
- 多重例外類型 是 Python 錯誤處理的核心技巧,能讓我們針對不同失敗原因提供恰當的回應。
- 熟悉例外類別的階層、使用
except (A, B)合併相同處理、以及分段捕捉不同例外,是寫出健全、易除錯程式的基本功。 - 搭配
else、finally、with以及 自訂例外,可以進一步提升程式的可讀性與資源安全性。 - 在實務開發中,務必先思考要捕捉哪些例外,避免過度寬鬆或過度嚴格,並遵循最佳實踐(如記錄錯誤、釋放資源)。
掌握了這些概念與技巧,你就能在日常開發、資料處理、網路服務等各種情境下,寫出更可靠、更具可維護性的 Python 程式。祝你在程式之路上除錯少一點、開發多一點! 🚀