前言
前面幾節處理例外的方式,都是在螢幕上print出錯誤訊息。但實務上有兩個問題:
- 程式如果是在背景執行(或交給使用者了),沒有人會看到螢幕上的訊息。
print(e)只顯示錯誤的結果,看不到「錯誤是從哪一行、經過哪些函式傳上來的」。
本節介紹兩個開發階段常用的工具:traceback(把完整的錯誤過程寫進檔案)與 assert(檢查程式的假設是否成立)。
traceback 記錄字串
利用traceback模組的 format_exc() 方法,可以將例外引發資訊的過程記錄在檔案中,以利追蹤錯誤引發的過程。
[簡例] 高速公路設定速限為 70~110,如果速度過快或太慢,就將主動拋出例外資訊的過程記錄在<err.txt>檔案中。(檔名:Traceback.py)
[結果]
錯誤資訊寫入完成!
目前時速:100
錯誤資訊寫入完成!程式碼:
Traceback.pyimport traceback def CheckSpeed(speed): # 檢查速度 if speed < 70: raise Exception("速度太慢了!") if speed > 110: raise Exception("已經超速了!") for speed in (60, 100, 150): try: CheckSpeed(speed) # 檢查速度 except Exception as e: # 接收 Exception 的例外 with open("err.txt", "a") as f: f.write(traceback.format_exc()) # 寫入例外過程 print("錯誤資訊寫入完成!") else: print("目前時速:{}".format(speed))
用記事本開啟<err.txt>檔,結果如下:
Traceback (most recent call last):
File "C:\example\ch07\Traceback.py", line 11, in <module>
CheckSpeed(speed) #檢查速度
File "C:\example\ch07\Traceback.py", line 5, in CheckSpeed
raise Exception("速度太慢了!") # 拋出 Exception 型別例外
Exception: 速度太慢了!說明
- 第
1行:匯入traceback模組。- 第
12~14行:接收Exception的例外,format_exc()方法將例外過程附加到<err.txt>檔案中。
為什麼要用 ‘a’ 模式
注意第 13 行用的是 **"a"(附加模式)**而不是"w"。因為迴圈會執行多次,若用"w",後面的錯誤會把前面的蓋掉,最後只剩最後一筆記錄。
錯誤記錄檔幾乎一定是用'a'模式。相關說明見 4. 文字檔資料的寫入與讀取 的新增模式。
怎麼讀 Traceback
Traceback 的閱讀順序是 由下往上:
| 位置 | 內容 | 意義 |
|---|---|---|
| 最下面一行 | Exception: 速度太慢了! | 錯誤的種類與訊息 |
| 倒數第二段 | line 5, in CheckSpeed | 真正出錯的那一行 |
| 上面各段 | line 11, in <module> | 一路呼叫過來的過程 |
先看最後一行,再看倒數第二段
新手常被一大串 Traceback 嚇到。其實最後一行告訴你「發生什麼事」,倒數第二段告訴你「在哪裡發生」 —— 這兩個看完,八成問題就定位了。
中間那些是呼叫過程,只有在追蹤複雜的函式呼叫時才需要細看。
format_exc()只能在except區塊裡使用它取得的是「目前正在處理的例外」的資訊。若在
except外面呼叫,會得到NoneType: None。
assert 斷言
assert主要用途是在程式開發階段,協助程式設計師檢查程式是否有指定的錯誤。
語法
assert 條件式, 參數程式執行時會檢查條件式:
- 條件式為
True→ 程式不予理會,繼續往下執行 - 條件式為
False(斷言失敗)→ 程式終止並拋出AssertionError的例外
為什麼需要 assert
先看看下列程式,Car類別以建構式設定初速,也可以Turbo()方法增加速度n。
class Car():
def __init__(self, speed):
self.speed = speed
def Turbo(self, n): # 增加速度 n
self.speed += n
for speed in (60, -20):
bus = Car(speed)
print("初速=", bus.speed, end=" ")
bus.Turbo(50)
print("加速後,速度=", bus.speed)[結果]
初速= 60 加速後,速度= 110
初速= -20 加速後,速度= 30初速 60 加速 50 後得到速度 110,同理初速 -20 加速 50 後得到速度 30。程式完全沒有報錯,但程式設計師知道,速度 < 0 顯然不合理。
這種錯誤就是 5. 例外處理 提過的「邏輯錯誤」—— 程式跑得動,但答案是錯的。加上assert就能把它抓出來。
[簡例] 建立Car類別並以建構式設定初速,Turbo()方法可以增加速度n,若初速 < 0,以assert斷言拋出此速度不可為負的例外。(檔名:Assert.py)
[結果]
初速= 60 加速後,速度= 110
初速= -20 Traceback (most recent call last):
File "C:\example\ch07\Assert.py", line 12, in <module>
bus.Turbo(50)
File "C:\example\ch07\Assert.py", line 6, in Turbo
assert self.speed >= 0, '速度不可能為負!'
AssertionError: 速度不可能為負!程式碼:
Assert.pyclass Car(): def __init__(self, speed): self.speed = speed def Turbo(self, n): # 增加速度 n assert self.speed >= 0, '速度不可能為負!' # ⭐ 斷言 self.speed += n for speed in (60, -20): bus = Car(speed) print("初速=", bus.speed, end=" ") bus.Turbo(50) print("加速後,速度=", bus.speed)
說明
- 第
6行:assert self.speed >= 0會檢查條件式,為True則不予理會繼續執行;為False則終止並拋出AssertionError。- 第
9~10行:分別以初速 60、-20 建立Car物件,初速 60 的物件可以正常加速,但初速為 -20 的物件則會引發第6行的例外。這樣就能在開發階段找出不合理的問題。
⚠️ 書上這裡寫的是
assert speed >= 0(少了self.)這是一個很值得討論的陷阱。
Turbo(self, n)的參數只有self和n,方法裡面根本沒有speed這個區域變數。那書上的程式為什麼跑得出正確結果?因為外面有一行
for speed in (60, -20):,建立了一個全域變數speed,Python 找不到區域變數時就往全域找,剛好抓到它。實測驗證:
# 情況一:沒有全域 speed Car(60).Turbo(50) # → NameError: name 'speed' is not defined # 情況二:全域 speed = 60,但物件初速是 -20 speed = 60 c = Car(-20); c.Turbo(50) # → 不報錯,speed 變成 30(斷言完全失效!)也就是說,書上的斷言檢查的是「迴圈變數」而不是「物件的速度」。在那個迴圈裡兩者剛好相等,所以看起來是對的 —— 但這是巧合。
正確寫法必須是
assert self.speed >= 0。 上面的範例已修正。
這個例子很適合拿來教「區域變數 vs 全域變數」以及「能跑 ≠ 正確」。
停用斷言
程式中如果到處充斥著assert,其實和用print印出錯誤問題相比也好不到哪去。可以在啟動 Python 直譯器時用 -O 引數來關閉assert。
語法:
python -O 程式檔.py以本例來說,請開啟命令視窗,切換到該目錄,輸入python -O Assert.py:
C:\example\ch07>python -O Assert.py
初速= 60 加速後,速度= 110
初速= -20 加速後,速度= 30可以看到斷言被略過,程式正常跑完。
⚠️ 因此
assert絕對不能用來做「使用者輸入檢查」因為
-O一下就把它全部關掉了。
用途 該用什麼 檢查程式設計師的假設(開發階段抓 bug) assert檢查使用者輸入是否合法 if+raise處理外部因素造成的錯誤(檔案不存在、網路斷線) try…except# ❌ 錯誤示範:使用者輸入用 assert 檢查 assert age >= 0, "年齡不可為負" # 加 -O 執行就形同虛設 # ✅ 正確:用 if + raise if age < 0: raise ValueError("年齡不可為負")
三種錯誤處理工具的比較
| 工具 | 對象 | 何時使用 | 上線後 |
|---|---|---|---|
try…except | 外部因素造成的例外 | 隨時 | 保留 |
raise | 違反業務規則 | 隨時 | 保留 |
assert | 程式設計師的假設 | 開發階段 | 可用-O關閉 |
traceback | 記錄例外過程 | 除錯/記錄 | 保留(寫成 log) |
相關主題與延伸閱讀
- 0. 章節索引:本章全部筆記的學習地圖與快速查找。
- 5. 例外處理:
try…except…else…finally的基本用法與三種錯誤類型。 - 11. raise 主動拋出例外:主動拋出內建或自訂的例外。
- 4. 文字檔資料的寫入與讀取:錯誤記錄檔要用附加模式
'a'寫入。 - 1. 物件導向程式開發:
assert範例中的類別與建構式觀念。