Logo

新人日誌

首頁關於我部落格

新人日誌

Logo

網站會不定期發佈技術筆記、職場心得相關的內容,歡迎關注本站!

網站
首頁關於我部落格
部落格
分類系列文

© 新人日誌. All rights reserved. 2020-present.

斷言(assertion)到底是什麼?從零開始搞懂程式裡的「斷言」

最後更新:2026年7月31日基礎概念

你可能在程式碼裡看過 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 精神一致,都是「宣告預期事實並自動驗證」,差別只在使用情境是測試碼還是產品碼
  • 判斷該用斷言還是一般錯誤處理的關鍵問題:這個條件不成立,是我的程式邏輯錯了,還是外部世界本來就可能這樣?

目前還沒有留言,成為第一個留言的人吧!

發表留言

留言將在審核後顯示。

基礎概念

目錄

  • 斷言是什麼?
  • 斷言跟一般的錯誤處理(if…raise)有什麼不同?
  • 有問題的寫法:用 assert 擋使用者輸入
  • 改善後的寫法:用一般錯誤處理面對可預期的錯誤
  • 斷言為什麼可以被關掉?它的執行時機是什麼?
  • 單元測試裡的斷言,跟前面說的是同一回事嗎?
  • 產品程式碼裡的斷言
  • 測試程式碼裡的斷言
  • 實務應用:什麼時候該用斷言?
  • 重點整理