Python 單元測試與除錯:assert 的應用
簡介
在日常開發中,我們常會遇到「這段程式碼理論上不會跑錯」的情況。為了確保這樣的前提成立,assert 提供了一個簡潔、直接的機制,讓程式在執行時即時檢查條件、在條件不符合時拋出 AssertionError。
- 即時回饋:在開發或測試階段,assert 能在錯誤發生的第一時間把問題顯示出來,避免 bug 靜默蔓延。
- 文件化假設:把對輸入、狀態或不變條件的假設寫在程式碼裡,讓閱讀程式的人一眼就能看出作者的期待。
雖然 assert 看似簡單,但若使用得當,能大幅提升程式的可讀性與可靠性;若濫用,則可能在正式環境中意外關閉檢查,造成更嚴重的錯誤。以下將從概念、範例、陷阱與最佳實踐等多面向,完整說明 assert 在 Python 中的正確應用方式。
核心概念
什麼是 assert
assert 是 Python 的關鍵字,語法為
assert <條件>, <錯誤訊息> # 錯誤訊息是可選的
當 <條件> 為 True 時,程式什麼也不做,直接往下執行;當 <條件> 為 False 時,會拋出 AssertionError,並顯示可選的錯誤訊息。
注意:在執行
python -O(或python -OO)時,所有assert會被 Python 優化器移除,等同於不寫任何檢查。因此,assert 應只用於開發、測試或文件化假設,而非取代正式的錯誤處理。
程式碼範例
1️⃣ 基本用法
# 基本的 assert 範例
x = 5
assert x > 0 # 若 x <= 0,會拋出 AssertionError
print("x is a positive number")
說明:x > 0 為真,程式順利印出訊息;若改成 x = -3,執行時即會中斷,提示開發者「斷言失敗」的地方。
2️⃣ 加上自訂錯誤訊息
def divide(a, b):
# 確保除數不為零
assert b != 0, "除數 b 不能為零!"
return a / b
print(divide(10, 2)) # 正常
print(divide(10, 0)) # AssertionError: 除數 b 不能為零!
說明:自訂訊息讓錯誤資訊更具可讀性,快速定位問題根源。
3️⃣ 在函式中檢查參數型別
def concatenate(str1, str2):
# 斷言兩個參數皆為字串
assert isinstance(str1, str) and isinstance(str2, str), \
"兩個參數都必須是 str 型別"
return str1 + str2
print(concatenate("Hello, ", "World!"))
# print(concatenate("Hello, ", 123)) # AssertionError
說明:利用 isinstance 檢查型別,可在函式入口即捕獲不符合預期的呼叫方式。
4️⃣ 檢查不變條件(Invariant)
class Stack:
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
# 不變條件:堆疊長度永遠非負
assert len(self._items) >= 0, "Stack length became negative!"
def pop(self):
assert self._items, "不能從空堆疊 pop!"
return self._items.pop()
說明:在資料結構或演算法中,不變條件 是維持正確性的關鍵。使用 assert 可以在每次操作後即時驗證這些條件。
5️⃣ 與 -O(optimize)選項配合
def compute(value):
# 這裡的 assert 僅在開發階段有效
assert value >= 0, "value 必須是非負數"
# 正式環境下仍會正常運算
return value * 2
# 執行方式:
# python script.py # 會檢查 assert
# python -O script.py # 會跳過所有 assert
說明:在正式部署時,可透過 -O 關閉 assert,減少執行時的額外檢查成本;但前提是程式已在開發階段充分測試,確保不依賴 assert 來保護關鍵邏輯。
常見陷阱與最佳實踐
| 陷阱 | 為什麼會發生 | 解決方式 |
|---|---|---|
| 把 assert 當作正式的錯誤處理 | 在 -O 模式下斷言會被移除,程式不會再拋出 AssertionError。 |
僅在開發或測試環境使用;對外部輸入或 API 需要的檢查,請改用 if ...: raise ValueError(...)。 |
| 斷言條件過於複雜 | 複雜的表達式不易閱讀,且在失敗時提供的訊息有限。 | 把條件拆成多行或使用輔助函式,並提供明確的錯誤訊息。 |
| 在效能敏感的迴圈裡大量使用 assert | 即使在非 -O 模式,assert 仍會執行,可能影響效能。 |
僅在關鍵路徑使用簡短檢查,或在正式環境以 -O 關閉。 |
| 忘記加上訊息 | 失敗時只能看到「AssertionError」而無法快速定位問題。 | 始終 為斷言加上具體、可讀的訊息。 |
| 斷言副作用 | 斷言內部若有函式呼叫,-O 時會被省略,可能改變程式行為。 |
斷言表達式 絕對 不應包含會改變狀態的副作用(如 I/O、寫檔)。 |
最佳實踐清單
- 明確、簡潔:斷言條件保持單一目的,配合具體訊息。
- 僅限開發:把斷言視為「開發時的安全網」,正式環境以例外處理或驗證函式取代。
- 避免副作用:斷言內部不要執行會改變程式狀態的程式碼。
- 配合測試框架:在單元測試 (
unittest,pytest) 中,assert可直接作為測試斷言;但在測試之外仍建議使用測試框架提供的斷言函式,以獲得更豐富的報告。 - 使用
-O警示:在部署腳本或 CI/CD 流程中加入python -O測試,確保程式不依賴 assert。
實際應用場景
演算法開發
- 在實作排序、搜尋等演算法時,使用 assert 檢查輸入序列是否已排序、索引是否在合理範圍。
資料前處理
- 讀取 CSV 檔案後,assert 欄位數量、資料型別符合預期,快速捕捉資料異常。
物件導向設計
- 在類別的建構子 (
__init__) 中斷言屬性不為None,或在方法入口斷言狀態符合不變條件。
- 在類別的建構子 (
外部函式庫的包裝
- 包裝 C 擴充模組或第三方 API 時,使用 assert 確認回傳值的型別與範圍,協助開發者在早期發現錯誤。
測試驅動開發(TDD)
- 在編寫測試時,直接使用
assert斷言結果;同時在實作階段,加入assert以確保程式行為與測試假設一致。
- 在編寫測試時,直接使用
總結
- assert 是 Python 內建、語法簡潔的「自我檢查」工具,適合在開發與測試階段快速驗證程式假設。
- 正確的使用方式是:僅作為開發時的安全網,配合具體的錯誤訊息,且絕不在生產環境依賴它。
- 了解
python -O會移除所有 assert,是避免在正式環境中遺漏檢查的關鍵。 - 透過本篇提供的範例與最佳實踐,你可以在日常開發、演算法實作、資料處理以及測試驅動開發中,善用 assert 提升程式的可讀性與可靠性,同時保持效能與維護性。
把 assert 當作「程式碼的註解」來看待——它不僅說明了作者的意圖,更在執行時即時提醒我們何時偏離了預期。願你在 Python 的單元測試與除錯旅程中,活用這把簡潔而有力的利器!