前言

前面幾節處理例外的方式,都是在螢幕上print出錯誤訊息。但實務上有兩個問題:

  1. 程式如果是在背景執行(或交給使用者了),沒有人會看到螢幕上的訊息。
  2. print(e)只顯示錯誤的結果,看不到「錯誤是從哪一行、經過哪些函式傳上來的」。

本節介紹兩個開發階段常用的工具:traceback(把完整的錯誤過程寫進檔案)與 assert(檢查程式的假設是否成立)。

traceback 記錄字串

利用traceback模組的 format_exc() 方法,可以將例外引發資訊的過程記錄在檔案中,以利追蹤錯誤引發的過程。

[簡例] 高速公路設定速限為 70~110,如果速度過快或太慢,就將主動拋出例外資訊的過程記錄在<err.txt>檔案中。(檔名:Traceback.py

[結果]

錯誤資訊寫入完成!
目前時速:100
錯誤資訊寫入完成!

用記事本開啟<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. 1行:匯入traceback模組。
  2. 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: 速度不可能為負!

說明

  1. 6行:assert self.speed >= 0會檢查條件式,為True則不予理會繼續執行;為False則終止並拋出AssertionError
  2. 9~10行:分別以初速 60、-20 建立Car物件,初速 60 的物件可以正常加速,但初速為 -20 的物件則會引發第6行的例外。這樣就能在開發階段找出不合理的問題。

⚠️ 書上這裡寫的是 assert speed >= 0(少了 self.

這是一個很值得討論的陷阱Turbo(self, n)的參數只有selfn,方法裡面根本沒有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)

相關主題與延伸閱讀