💡 先搞懂問題
虛構的「北辰補習班」有一支小程式,替每位學生算平均分數、印成報表。前幾天都好好的,今天新來的學生阿哲還沒考過試,分數串列是空的,按下執行,畫面先印出小美的平均,接著跳出一大段紅字,最後的「報表完成」沒有出現。第一次看到這種畫面的人,常見的反應是整段跳過、直接去改程式碼,或是只看第一行的 Traceback (most recent call last),看不懂就放棄。
其實這段紅字是有固定格式的報告,叫 Traceback(追蹤記錄,也常直接叫錯誤訊息)。它的重點全在最後一行:冒號前是錯誤類型,冒號後是一句說明;往上每兩三行一組,告訴你出錯當下程式是從哪一行、經過哪個函式一路走到出事的地方。只要知道從哪裡開始讀,大部分錯誤訊息都會變得很好懂。
average 函式的第 3 行;再往上兩組說明它是被 report 的第 5 行呼叫、而 report 又是被主程式第 8 行呼叫的。~ 和 ^ 標出那一行裡出事的位置。生活比喻:包裹的配送失敗通知單
你寄了一個包裹,幾天後收到一張配送失敗通知單。單子上由上往下列出包裹經過的每一站:台北物流中心收件、台中轉運站分揀、豐原配送站派送,最後一行用粗體寫著失敗原因:「查無此地址」。你不會從第一行開始慢慢研究台北物流中心,而是直接看最後一行的原因,再往上看是在哪一站失敗的。如果原因是地址寫錯,問題其實出在你寄件時填的單子,不是配送站做錯事;配送站只是第一個發現問題的地方。
File "…", line 8, in <module>:哪個檔案、第幾行、在哪個函式裡(<module> 指的是最外層的主程式),下一行是那一行的程式碼。開頭那句 most recent call last 的意思是「最近的呼叫寫在最後」,所以越下面越接近出事的地方。最後一行的失敗原因,就是錯誤類型(exception type)和說明,例如 ZeroDivisionError: division by zero(除以零)。在 Python 裡,執行中發生的錯誤叫例外(exception),Python 說它被「拋出(raise)」了。
這個比喻有兩個地方和程式不同。第一,通知單上的每一站都已經做完自己的工作,Traceback 裡的每一層卻是「還在等待、被迫中止」的函式呼叫(第 9 課的呼叫框):主程式在第 8 行等 report,report 在第 5 行等 average,錯誤一發生,三層全部停下。第二,有一種錯誤像是包裹在寄件櫃台就被退回、根本沒出貨:程式寫法本身不合 Python 的文法(語法錯誤),Python 在開始執行前就會拒絕,一行都不會跑。還有一種錯誤連通知單都沒有:包裹準時送到了,打開卻發現寄錯東西,也就是程式順利跑完、答案卻是錯的。這三種情況,這一課都會一一處理。
🎮 互動實驗室一:錯誤從函式裡一層層往外傳
這是上面那張錯誤畫面的程式,用 Python 3.13 真的執行、逐步記錄下來。第一次呼叫 report 一切正常;第二次傳進空串列,錯誤在最裡層的 average 發生。請注意左欄的呼叫框:錯誤沒有被處理時,最上層(最新)的框會被迫中止、拿掉,錯誤交給下一層,直到主程式也停下。停在 🤔 的地方,先猜再按下一步。
report、average),和錯誤畫面裡的三組 File 一一對應。第 18 步發生錯誤,接下來每按一次,就有一個框被迫中止、拿掉,錯誤往外交一層。最後一步輸出畫面出現紅字,你會發現「報表完成」始終沒有印出:例外沒被處理,整支程式就停在那裡。🎮 互動實驗室二:用 try/except 接住轉換失敗
請使用者輸入年齡,再用 int() 轉成整數(第 3 課),是最常見的出錯場景:input() 拿到的永遠是字串,使用者只要打「二十五」或「25.5」,轉換就失敗。右上角選單有四種情況:第一種沒有防護,後三種把可能出錯的那一行放進 try,並準備一段 except ValueError: 處理轉換失敗。比較它們的執行路線:綠色箭頭會跳過哪幾行。
int(text) 出錯,但下一步綠色箭頭跳到 except ValueError:,印出提示後繼續執行最後一行,程式正常結束。切到「輸入 25」,則是 except 底下兩行完全被跳過。age 這個名字只有在轉換成功時才會出現在記憶體裡。🎮 互動實驗室三:錯誤診斷卡
12 張卡片,每張是一小段程式和它在 Python 3.13 執行後的真實畫面。照這一課的方法讀:先看最後一行的錯誤類型與說明,再找行號和 ^ 標出的位置,然後選出最合適的判斷或改法。其中有一張完全沒有錯誤訊息,要靠你自己發現答案不對。
📘 原理補完
1. 讀 Traceback 的四個步驟
- 看最後一行冒號前面:錯誤類型,告訴你是「哪一類」問題,例如名字不存在(NameError)、型別不對(TypeError)。
- 看最後一行冒號後面:說明,常常直接寫出是哪個名字、哪個值,例如
'摩卡'、'3.5';新版 Python 還會加上Did you mean: …?的建議。 - 往上找最下面那一組 File:檔名、第幾行、在哪個函式裡(
in <module>是主程式),下一行就是出事的程式碼,^和~標出位置。 - 再往上看呼叫過程:只有錯誤發生在函式裡時才會有好幾組;它們告訴你是誰把這個值傳進來的,根源常常在這裡。
~ 和 ^ 是 Python 3.11 起加入的位置標記:^ 指出真正出事的那一個動作,這裡是加號;~ 標出參與這個動作的其他部分,也就是加號兩邊的 "你今年" 和 age。合起來讀就是:「字串加整數」這個加法做不了。標記不是每次都有,有些行 Python 不畫(例如實驗室一的 avg = average(scores)),這時就把整行一起看。2. 三種錯誤:沒開始、跑一半、跑完卻錯
錯誤依照「什麼時候被發現」分成三種,處理方法完全不同。Python 執行 .py 檔時,會先把整個檔案讀一遍,確認每一行都符合 Python 的文法,這一關沒過就是語法錯誤(syntax error),一行都不會執行。文法沒問題,開始一行一行執行,執行到某一行做不下去(除以零、找不到名字),就拋出例外(exception),在那一行停下,這叫執行時錯誤。第三種是邏輯錯誤(logic error):每一行都順利執行完,但程式做的事和你想的不一樣,Python 不會給任何訊息。
| 種類 | 什麼時候發現 | 前面的行有沒有執行 | 畫面長怎樣 | 例子 |
|---|---|---|---|---|
| 語法錯誤 | 執行前的檢查階段 | 沒有,一行都不執行 | 沒有 Traceback 開頭,直接是 File、程式碼、標記、SyntaxError 或 IndentationError | 少冒號、括號沒關、縮排不一致 |
| 執行時錯誤(例外) | 執行到那一行時 | 有,執行到出錯為止 | Traceback (most recent call last): 開頭,最後一行是錯誤類型與說明 | 名字拼錯、字串加數字、除以零、索引超出範圍 |
| 邏輯錯誤 | 你自己檢查結果時 | 全部執行完 | 沒有任何錯誤訊息,只是答案不對 | range 少一圈、條件寫反、用錯變數 |
因為語法錯誤發生在執行之前,播放器沒辦法逐行展示它,下面是 Python 3.13 實際的畫面。注意它們沒有 Traceback 那一行,因為程式根本還沒開始呼叫任何東西。File 後面的路徑是這支檔案在電腦上的位置(本頁的範例是在 Linux 的 /tmp 資料夾執行;Windows 上會像 C:\Users\你的名字\main.py)。
if 那一行少了冒號:Python 直接說 expected ':'(這裡應該要有冒號),^ 指在行尾。
price = 120
if price > 100
print("滿百")
File "/tmp/b0_err/main.py", line 2
if price > 100
^
SyntaxError: expected ':'
括號沒有關:錯誤指的是「左括號從來沒有關上」,位置在那個左括號。注意輸出畫面上連第 1 行的「第一行」都沒有出現。
print("第一行")
print("第二行"
print("第三行")
File "/tmp/b0_err/main.py", line 2
print("第二行"
^
SyntaxError: '(' was never closed
函式(或 if、for)底下沒有縮排:IndentationError 是語法錯誤的一種,專門指縮排的問題。說明裡連「第 1 行是函式定義」都告訴你了:
def greet(name):
print("歡迎", name)
File "/tmp/b0_err/main.py", line 2
print("歡迎", name)
^^^^^
IndentationError: expected an indented block after function definition on line 1
不該縮排的地方多了空白:unexpected indent,意思是「這裡不該縮排」。常見於從網頁複製程式時多帶了空白。
print("開始")
print("多了空白")
File "/tmp/b0_err/main.py", line 2
print("多了空白")
IndentationError: unexpected indent
語法錯誤的位置有時會比真正的錯誤晚一點:例如上一行少了右括號,Python 可能要讀到下一行才確定出問題。看到語法錯誤時,除了標出的那一行,也要檢查它的上一行。
3. 常見的執行時錯誤一覽
下面右欄都是 Python 3.13 實際執行後的最後一行,程式碼和完整畫面在實驗室三的卡片裡。遇到不認識的錯誤類型,可以先查這張表,再到 官方的內建例外清單 找。
| 錯誤類型 | 白話意思 | 新手最常見的原因 | Python 3.13 的最後一行 |
|---|---|---|---|
| NameError | 找不到這個名字 | 拼錯字、大小寫不同、還沒指派就使用、用了函式裡的區域變數 | NameError: name 'totl_price' is not defined. Did you mean: 'total_price'? |
| TypeError | 這個型別不能這樣用 | 字串加數字、對 None 做運算、呼叫函式時引數個數不對 | TypeError: can only concatenate str (not "int") to str |
| ValueError | 型別對,但值不合法 | int() 轉換有小數點或中文的字串 | ValueError: invalid literal for int() with base 10: '3.5' |
| IndexError | 索引超出範圍 | 忘了索引從 0 開始,用 len() 當索引 | IndexError: list index out of range |
| KeyError | 字典裡沒有這個鍵 | 鍵拼錯、資料裡本來就沒有這筆 | KeyError: '摩卡' |
| ZeroDivisionError | 除以零 | 計數或長度剛好是 0,例如空串列的平均 | ZeroDivisionError: division by zero |
| AttributeError | 這種物件沒有這個方法或屬性 | 方法名稱拼錯、把別的語言的寫法帶進來 | AttributeError: 'str' object has no attribute 'uper'. Did you mean: 'upper'? |
表格裡有兩行出現 Did you mean:拼錯的名字或方法和某個存在的名字很像時,Python(3.10 起)會主動猜你想打的是哪一個。這個建議通常是對的,但它只是猜測,例如實驗室三的 cart.push() 就沒有任何建議,因為串列的方法裡沒有和 push 夠像的名字,你得自己知道串列加東西是用 append(第 5 課)。
4. 錯誤發生在函式裡:Traceback 有好幾層
第 9 課說過,函式呼叫函式時,呼叫框會疊起來。例外在最上層(最新)的框發生,如果那個函式沒有處理它,函式就被迫中止、框被拿掉,例外交給呼叫它的那一行;那一行如果也沒處理,就再往外交,一直到主程式。主程式也沒處理,整支程式停止,印出 Traceback。所以 Traceback 的每一組 File,就是錯誤經過的每一個框,順序由外到內。
average 第 3 行除以零,交給 report 第 5 行,再交給主程式第 8 行,三層都沒有 try/except,程式停止。右邊是對應的 Traceback 三組 File,順序和左邊由上往下的框一致。讀多層 Traceback 時,最下面那組告訴你「哪個動作做不下去」,往上的幾組告訴你「這個不合理的值是從哪裡來的」。這個例子裡,除法本身沒寫錯,真正該決定的是:空串列要不要算平均?可以在 average 裡先檢查 len(scores) == 0,或在呼叫 report 之前就把沒有成績的學生排除。錯誤訊息指出的是第一個發現問題的地方,修在哪裡要由你判斷。
5. 沒有錯誤訊息,答案卻不對:邏輯錯誤與除錯方法
想算三次考試的總分,70 + 85 + 90 應該是 245,下面的程式卻印出 175,而且沒有任何紅字。這時沒有錯誤訊息可以讀,只能自己找。最有效的兩個方法都在下面這個播放器裡:一是逐行執行,看每一步名字貼到什麼;二是在可疑的地方加 print(),把迴圈每一圈的變數印出來,和你心裡預期的值比對。選單第二段就是加了 print 的版本,第三段是修正後的版本。
i 第一次貼上的值:它是 1,不是 0,所以 scores[0] 的 70 從來沒被加進去。加了 print 的版本會在輸出畫面印出每一圈的 i,第一行就是 i = 1,問題一眼就看得到。修正版把 range(1, len(scores)) 改成 range(len(scores))。另外你可能會發現,total 加完之後的箭頭有時指向串列格子裡的同一個數字物件(例如 85),修正版一開始 i 和 total 也指向同一個 0:這是 Python 重用了相同的不可變整數,因為它不能被修改,共用不會出問題。播放器以外,你自己的電腦也能逐行執行:VS Code 等編輯器(見 編輯器與 IDE 節點)都有除錯器(debugger),在行號旁點一下設中斷點,程式就會停在那裡讓你看變數。不論用哪種工具,除錯的流程都差不多:
把錯誤拿去搜尋或問 AI 時要附上什麼。直接貼「我的程式壞了」很難得到好答案。請附上:(1) 完整的 Traceback,從 Traceback 那一行到最後一行,不要只貼最後一行,也不要自己改寫;(2) 出錯的那幾行程式,加上它用到的變數是怎麼來的;(3) Python 版本(在終端機打 python --version)與執行環境(VS Code、Colab…);(4) 你預期的結果和實際的結果;(5) 你已經試過什麼。邏輯錯誤沒有 Traceback,第 (4) 點就特別重要。貼之前,把密碼、API 金鑰、客戶資料這類內容刪掉或換成假的。搜尋引擎則適合只貼最後一行,並把自己的變數名稱、檔案路徑拿掉,例如搜尋 TypeError: can only concatenate str (not "int") to str。
6. try/except 入門:預料得到的錯誤,先準備好處理方式
有些錯誤不是程式寫錯,而是外面的資料不照規矩來,例如使用者亂打、檔案內容有一筆壞掉。這種「預料得到、也知道怎麼處理」的錯誤,可以用 try/except 接住:把可能出錯的程式放在 try: 底下(要縮排),在 except 錯誤類型: 底下寫出錯時要做的事。try 裡某一行拋出例外,這一行和 try 裡剩下的行都不再執行,直接跳到類型相符的 except;沒有出錯,except 整段跳過。不論哪條路,之後都繼續往下執行。
try 全部執行,跳過 except。紅色是轉換失敗:在 int(text) 那一行就離開 try,age 沒有被建立,下一行的 print 也不執行,改執行 except ValueError: 底下的提示。兩條路最後都走到「程式繼續執行」。第一個完整範例是實務上很常見的寫法:用 while True 一直問,直到使用者輸入正確為止(while 和 break 是第 8 課的內容)。下面的輸出是模擬使用者依序輸入「二十五」「25.5」「25」時,Python 3.13 實際執行的畫面(直接執行時,你打的字會出現在提示後面;這裡輸入是從檔案讀進來的,所以畫面上看不到你打的字)。
# 一直詢問,直到使用者輸入整數為止
while True:
text = input("請輸入年齡:")
try:
age = int(text) # 可能拋出 ValueError 的那一行
break # 上一行成功才會執行到這裡:跳出 while 迴圈
except ValueError: # 只接住「轉換失敗」這一種錯誤
print("看不懂「" + text + "」,請輸入整數,例如 25")
print("明年你就", age + 1, "歲")
請輸入年齡:看不懂「二十五」,請輸入整數,例如 25 請輸入年齡:看不懂「25.5」,請輸入整數,例如 25 請輸入年齡:明年你就 26 歲
第二個範例處理一批資料:其中有幾筆壞掉,不希望整批停下來,就逐筆轉換,壞的那筆記下來、跳過:
# 一批從表單收到的訂購杯數,其中有幾筆填錯
raw = ["2", "1", "三", "4", "", "3"]
total = 0
bad = [] # 記下轉換失敗的那幾筆
for t in raw:
try:
total = total + int(t) # int("三")、int("") 都會拋出 ValueError
except ValueError:
bad.append(t) # 這一筆跳過,繼續處理下一筆
print("有效杯數合計:", total)
print("無法辨識:", bad)
有效杯數合計: 10 無法辨識: ['三', '']
except:(不寫錯誤類型)或 except Exception: 加一行 pass,讓紅字消失。這樣程式看起來不會壞,但名字拼錯、型別用錯這類真正的 bug 也一起被吞掉,結果默默算錯,比當場報錯更難找。原則是:只接你預料得到、而且知道怎麼處理的錯誤類型,其他錯誤就讓它照常顯示。try/except 的進階用法(else、finally、自己拋出例外)在 例外處理 節點。7. 以後會在哪裡遇到
讀 AI 或資料分析的範例程式時,最常見的錯誤其實就是這一課的這幾種:資料表裡沒有那個欄位名稱是 KeyError,把文字欄位拿去計算是 TypeError 或 ValueError,模型輸入的形狀不對時,套件也會在最後一行寫出期待的形狀和實際的形狀。套件的 Traceback 往往很長,中間有很多層是套件自己的程式碼,讀法還是一樣:先看最後一行,再往上找第一組你自己的檔案,那通常就是該修改的地方。
✅ 自我檢測
6 題原創的程式閱讀題,所有答案都用 Python 3.13 實際執行確認過;輸出有好幾行時,選項裡用 ⏎ 表示換行。選完會立即顯示對錯與解析,全部作答後出現總分。目前得分:0 / 6