本篇是故事三的「重現」篇。
本篇要回答:錯誤型別為什麼能傳這麼遠?第一個把它擋下來的邊界在哪裡?
Day 11 說錯誤在「很下游」才現形。這一篇回答為什麼:因為中間每一層都對 tuple 沒有意見。
事件軌跡:
變數賦值(型別在這裡變形)
↓
函式參數(只要求「可迭代」,tuple 完全合格)
↓
資料轉換(len、sum、迴圈——全部照常運作)
↓
序列化、ORM 或 API 邊界(結構第一次被寫死)
↓
下游才發現型別或結構不符
我原以為錯的資料會很快撞牆。實際上 Python 的慣例是鴨子型別:函式普遍只要求「行為像」,不要求「就是」。這是語言的優點——同一個特性,讓正確的多型與錯誤的湊合都暢通無阻。
最小重現,每一步都實際執行過:
value = 42
result = value, # (42,)
len(result) # 1 —— 合法,只是意義已經不對
[x for x in result] # [42] —— 可迭代,照常通過
def total(items):
return sum(len(str(i)) for i in items)
total(("abc",)) # 3 —— 接受任何 iterable 的函式毫無意見
total("abc") # 3 —— 順帶一提:字串也是 iterable,另一個經典陷阱
import json
json.dumps((42,)) # '[42]' —— 邊界第一次改變資料形狀
注意最後一行:json.dumps 不報錯,它把 tuple 靜靜序列化成 JSON 陣列。原本下游期待的 42 變成了 [42]——第一個「把結構寫死」的邊界不是擋下錯誤,而是把錯誤翻譯成另一種格式送出去。真正的例外要等到更下游有人對 [42] 做數值運算或 schema 驗證時才引爆,而那裡的 Stack Trace 不會指向逗號。
三個核心問題的答案於是清楚了:哪些函式沒有立即失敗?所有只要求 iterable 的。哪個邊界第一次要求精確結構?序列化與資料契約邊界。錯誤發生點與根因相距多遠?隔著整條呼叫鏈加一次格式轉換。
區分證據等級。已確認事實:上述每一行的行為(已執行驗證)。合理推論:當時的事故沿同型路徑滑行。
本篇結論:
真正需要重現的不是最後一個例外,而是錯誤型別如何一路被當成正常資料傳下去。
下一篇(Day 14)把這條路徑翻譯成證據的語言:語法正確、程式可跑、資料契約正確,是三種不同的證據。