你可能在程式碼裡看過 assert 這個關鍵字,但完全不知道它在做什麼。
你可能也想過——這跟 if...throw 錯誤處理有什麼不一樣?為什麼不乾脆都用 if 就好?
你甚至可能聽過「單元測試裡也有斷言」,想問這跟前面說的斷言是同一件事嗎?
這篇文章就從最基本的定義開始,把這些疑問一次講清楚。
斷言是什麼?
assertion 這個英文字,原本的意思就是「堅定地宣稱、斷定某件事是真的」,你可以想成是一個人拍胸脯保證「這件事一定是這樣」的語氣。
在日常英文裡,這個字常常這樣用:「He made the assertion that the plan would work.」(他斷言這個計畫一定會成功。)
注意這裡的語氣是很篤定的,講的人幾乎沒有懷疑的空間,這也是為什麼程式裡借用這個字,而不是用「猜測」或「檢查」這類語氣比較保留的詞。
程式裡的斷言(assertion)延續了這個語感:你在程式碼裡插入一句話,告訴電腦「我認為這裡的狀態一定是這樣,如果不是,代表程式有 bug」。
換句話說,斷言不是用來處理「使用者可能會犯的錯」,而是用來抓「你自己寫程式時的假設有沒有錯」。
你可以把它想成寫程式時,你在心裡默默假設「執行到這裡的時候,這個變數一定是正數」、「這個陣列一定不會是空的」。
斷言就是把這些「你以為理所當然」的假設,寫成一行程式碼,讓電腦幫你檢查這個假設是不是真的成立。
來看最基本的例子,以 Python 的 assert 為例:
def calculate_discount(price, discount_rate):
assert discount_rate >= 0
assert discount_rate <= 1
return price * (1 - discount_rate)
calculate_discount(100, 0.2) # 正常執行,回傳 80
calculate_discount(100, 1.5) # AssertionError,程式直接中斷這段程式碼裡的兩個 assert 在說:「我假設 discount_rate 一定介於 0 到 1 之間」。
如果有人不小心傳進 1.5(150% 的折扣,不合理),assert 會立刻讓程式中斷,並丟出 AssertionError。
重點是:斷言檢查的是「不應該發生的狀態」,一旦真的發生,代表程式邏輯本身有問題,而不是外部輸入單純不合規。
斷言跟一般的錯誤處理(if…raise)有什麼不同?
這是最多人搞混的地方,我們先把兩者的定位講清楚。
一般的錯誤處理(例如 if 判斷加上 raise 或 throw)是用來處理「可預期、可能發生」的狀況,像是使用者輸入錯誤、網路連線失敗、檔案不存在。
這些狀況你「本來就知道有可能發生」,所以程式需要優雅地應對,給使用者友善的錯誤訊息,甚至讓程式繼續運作。
斷言則相反,它是用來檢查「照理說不可能發生」的狀況。
如果斷言失敗了,代表的不是使用者做錯事,而是「你身為開發者的假設錯了」,也就是程式本身有 bug。
我們用一個對比來看差異:
有問題的寫法:用 assert 擋使用者輸入
def withdraw(balance, amount):
# ❌ 有問題的寫法:把「使用者可能犯的錯」拿去用 assert 檢查
assert amount <= balance, "餘額不足"
return balance - amount先看這段程式碼實際執行起來會發生什麼事。
假設使用者帳戶餘額是 100 元,他想領 500 元,於是程式呼叫 withdraw(100, 500)。
因為 500 大於 100,assert amount <= balance 這個條件不成立,於是 Python 會直接丟出這樣的錯誤,並且讓整個程式當場中斷:
Traceback (most recent call last):
File "app.py", line 10, in <module>
withdraw(100, 500)
File "app.py", line 2, in withdraw
assert amount <= balance, "餘額不足"
AssertionError: 餘額不足問題就出在這裡:「使用者想領的錢比餘額多」根本不是什麼罕見的意外,而是你在設計提款功能時,一開始就該預料到、每天都會發生無數次的正常情境。
既然是正常情境,程式就應該「好好地」回應使用者,例如在畫面上顯示一句「餘額不足,請重新輸入金額」,讓使用者可以修正之後再試一次。
但 assert 做不到這件事,它的行為是「條件不成立就直接讓整個程式當掉,並拋出一長串技術性的錯誤訊息」。
這種體驗放到正式產品裡是不能接受的,你不可能讓使用者看到上面那串 Traceback。
換句話說,assert 這個工具天生的行為就是「發現不對勁,立刻讓程式中斷」。
這種行為適合拿來抓「開發者自己邏輯寫錯」的情況,因為那種情況本來就應該讓程式停下來,逼你回頭修 bug。
但拿來面對「使用者操作上的正常錯誤」就不合適,因為使用者的錯誤是可以預期、也應該被優雅處理的,不該讓整個程式當掉。
而且還有一個更嚴重的風險:像 Python 這類語言可以用執行參數(例如 -O)把整份程式裡所有的 assert 全部關掉。
關掉之後,這些 assert 那一行會被當作完全不存在,直接跳過不執行。
這代表如果你把「餘額夠不夠」這種原本應該永遠都要檢查的邏輯,寫在 assert 裡面,一旦正式環境用 -O 模式啟動,這個檢查就會整個消失。
結果就是使用者不管想領多少錢,程式都不會擋,形成一個資安漏洞。
改善後的寫法:用一般錯誤處理面對可預期的錯誤
def withdraw(balance, amount):
# ✅ 改善後的寫法:用一般錯誤處理來面對「可預期的錯誤」
if amount > balance:
raise ValueError("餘額不足")
return balance - amount
def transfer_internal(source_balance, amount):
# assert 用在「這是我方邏輯的假設,理論上不該出錯」的地方
assert source_balance >= 0, "系統內部餘額不應該為負,這是程式 bug"
return source_balance - amount這樣寫的好處是:「使用者可能犯的錯」用 raise 明確處理,並且不管在什麼環境下都會執行。
而「開發者對系統狀態的假設」則用 assert,一旦這個假設被打破,代表程式邏輯本身壞了,需要盡快修。
簡單一句話記住這個分野:能預期、該對外負責的錯,用一般錯誤處理;不該發生、只對自己開發階段負責的錯,用斷言。
斷言為什麼可以被關掉?它的執行時機是什麼?
前面提到斷言可以被整組關掉,這其實是斷言的一個核心特性,而不是缺點。
因為斷言的設計目的是「開發跟測試階段拿來抓自己邏輯錯誤」,一旦程式驗證過沒問題,正式上線後,理論上這些假設都已經成立,不需要每次執行都重新檢查一次,浪費效能。
所以像 Python 這種語言允許你在正式環境用 python -O 啟動程式時,直接把所有 assert 語句跳過不執行,藉此換取效能。
這也再次呼應前面說的:如果你把「使用者輸入驗證」這種正式環境也一定要做的檢查寫在 assert 裡面,一旦正式環境用 -O 關掉斷言,這個檢查就會整個消失。
而你可能完全沒發現。
# 用 assert 做的檢查,在 -O 模式下會被完全跳過
assert user_input_length > 0
# 用 if 做的檢查,不管什麼模式都一定會執行
if user_input_length <= 0:
raise ValueError("輸入不能為空")這段程式碼的重點在於:兩者看起來都在「檢查一個條件」,但底層的執行保證完全不同。
用 assert 寫的檢查,只在開發、測試階段幫你抓邏輯錯誤。
用 if 寫的檢查,則是你對外部世界(使用者、網路、檔案)承諾一定會做的把關。
兩者不能互相取代。
單元測試裡的斷言,跟前面說的是同一回事嗎?
單元測試(unit test)是另外寫一份程式碼,專門用來自動驗證你的某個函式或某個功能,執行起來是不是符合你的預期。
你可以把它想成,與其每次改完程式碼都自己手動點來點去、肉眼檢查結果對不對,不如寫一段程式碼,讓它自動幫你重複做這件事。
而測試程式碼要驗證「結果對不對」,靠的就是斷言。
你先告訴電腦「我認為這個函式跑完之後,結果應該是這樣」,然後讓電腦去比對實際跑出來的結果,跟你講的是不是一致。
這也是為什麼測試框架會用 assertEqual、expect(...).toBe(...) 這種帶有「斷言」語感的名字。
因為它們做的事情,跟前面講的 assert 骨子裡是同一件事:宣告一個你認為一定成立的事實,再讓電腦幫你確認。
差別只在於,前面看到的 assert 是嵌在「功能程式碼」裡,檢查的是程式執行過程中的狀態。
而這裡的斷言則是嵌在「測試程式碼」裡,檢查的是整個函式執行完之後的結果,這點我們接下來會更仔細拆開來看。
產品程式碼裡的斷言
前面在講「什麼時候不該用 assert」的時候,我們看到的 assert 都是寫在你實際要執行的功能程式碼裡面,例如提款功能、折扣計算。
這種程式碼會被使用者實際執行到,我們把它稱為「產品程式碼」。
也就是說,只要使用者打開你的 App、點了某個按鈕、送出了一筆請求,這份程式碼本身就會被觸發執行,這是產品程式碼原本就會發生的事。
而 assert 只是你額外埋在這份程式碼裡的檢查,它不會改變程式什麼時候被執行,只是在程式執行的過程中,多了一個順便確認狀態對不對的動作。
換句話說,這份程式碼「本來就有」assert,重點是:使用者觸發這份程式碼執行的當下,assert 也會跟著一起被執行到,而使用者完全不會知道背後多了這一道檢查。
在產品程式碼裡,assert 檢查的是「程式執行到這一行的當下,狀態是不是符合我的假設」,例如「餘額不應該是負數」。
你可以想成 assert 是埋在程式流程裡的一個小關卡。
程式一路往下執行,執行到 assert 這一行的時候,就會停下來看一眼「現在的狀態符不符合我的假設」,符合就繼續往下跑,不符合就立刻中斷。
這是在程式實際運作的過程中,順便幫你抓內部邏輯有沒有出錯。
這裡的「順便」是關鍵,因為這個檢查不是你特地啟動的,而是跟著程式正常執行的路徑一起發生,程式在做提款、算折扣的同時,assert 就在背後默默確認你原本的假設有沒有被打破。
也因為它是跟著正式流程一起跑的,一旦這裡的假設被打破,代表的不是某次單獨測試沒過,而是使用者實際使用產品的當下,你的程式邏輯真的出了問題。
測試程式碼裡的斷言
而測試程式碼是完全不同的一份程式碼,它平常不會被使用者執行到,只有在你開發的時候,自己手動執行測試指令才會跑起來。
測試程式碼的工作是:呼叫你寫好的函式,餵給它一組你自己設計的輸入,然後檢查回傳的結果跟你心裡預期的答案是不是一致。
這裡的 assert(或是其他測試框架的寫法,例如 assertEqual、expect(...).toBe(...))檢查的不是「程式執行到這裡的內部狀態」,而是「這個函式的最終輸出結果,符不符合我原本設計這個函式時想要的行為」。
換句話說,兩者檢查的對象不一樣。
產品程式碼裡的 assert 檢查的是「執行過程中的某個瞬間,狀態合不合理」;測試程式碼裡的 assert 檢查的是「整個函式跑完之後,結果對不對」。
用一個具體情境來看差異,假設你有一個 add(a, b) 函式:
def add(a, b):
result = a + b
# 這是產品程式碼裡的 assert:檢查執行過程中的內部假設
assert isinstance(result, (int, float)), "相加結果應該是數字"
return result這裡的 assert 是寫在 add 函式內部,它在檢查「這個函式執行到這裡的時候,計算出來的 result 應該是一個數字」,這是函式作者對自己實作邏輯的假設。
# 這是測試程式碼,跟上面的 add 函式本身是分開的檔案
def test_add():
result = add(2, 3)
assert result == 5 # 測試裡的 assert:檢查函式的最終輸出對不對這裡的 assert 是寫在測試檔案裡,它不關心 add 函式內部是怎麼運作的,只在乎「呼叫 add(2, 3) 之後,最後拿到的結果是不是 5」。
也就是只檢查輸出結果,不檢查過程。
所以你可以這樣分辨:如果這段 assert 是跟著功能程式碼一起執行、檢查的是執行過程中某個瞬間的內部狀態,那它是產品程式碼裡的斷言。
如果這段 assert 是寫在獨立的測試檔案裡、只在你手動跑測試時才會執行、檢查的是函式跑完之後的最終結果,那它就是測試裡的斷言。
如果這個 assert 失敗了,例如 add 函式不小心被改成回傳 a - b,那 test_add 裡的 assert result == 5 就會失敗。
測試框架會把這個結果標記為測試失敗,方便你回頭檢查程式碼哪裡壞了。
所以不管是產品程式碼裡的 assert,還是測試框架裡的 assertEqual,核心精神都相同:把「你認為理所當然成立的事」寫成程式碼,讓電腦自動幫你驗證。
而不是靠肉眼檢查或憑感覺相信程式是對的。
實務應用:什麼時候該用斷言?
了解定義之後,實務上最常見的使用場景整理如下。
適合用斷言的情境:
- 檢查函式的前置條件(precondition),例如「這個參數理論上一定是正數」
- 檢查函式執行完的後置條件(postcondition),例如「排序完的陣列一定是遞增的」
- 檢查程式內部不變量(invariant),例如「這個佇列的長度一定跟計數器一致」
- 撰寫單元測試時,驗證函式的輸出是否符合預期
不適合用斷言的情境:
- 驗證使用者輸入(應該用 if 搭配明確的錯誤訊息或例外處理)
- 檢查檔案是否存在、網路是否連得上這類「本來就可能失敗」的外部資源狀態
- 任何「正式環境也一定要執行」的檢查,因為斷言可能被關閉
取捨上要記住的核心原則是:斷言是開發者留給自己的除錯工具,不是系統對外的防護機制。
如果分不清楚該用哪一種,可以問自己一個問題:「如果這個條件不成立,是因為我的程式寫錯了,還是因為外部世界本來就可能這樣?」前者用 assert,後者用一般的錯誤處理。
重點整理
- 斷言(assertion)是開發者在程式碼裡寫下「我認為這裡一定成立」的假設,並讓電腦自動檢查
- 斷言檢查的是「不應該發生、代表程式有 bug」的狀態,不是用來處理使用者可能犯的錯
- 一般錯誤處理(if…raise)用來面對「可預期會發生」的狀況,例如使用者輸入錯誤;這兩者不能互相取代
- 斷言可能在正式環境被整組關閉(例如 Python 的
-O模式),所以絕對不能拿它做正式環境也必須執行的檢查(例如輸入驗證) - 單元測試裡的 assert(如 assertEqual)跟產品程式碼裡的 assert 精神一致,都是「宣告預期事實並自動驗證」,差別只在使用情境是測試碼還是產品碼
- 判斷該用斷言還是一般錯誤處理的關鍵問題:這個條件不成立,是我的程式邏輯錯了,還是外部世界本來就可能這樣?