2.41. 除錯¶
在相機上失敗的指令碼大多以三種方式之一失敗:拋出例外、產生錯誤的值,或卡住不動。每一種都有一組不同的工具來處理。
2.41.1. 讀懂回溯(traceback)¶
當指令碼拋出例外且沒有任何程式碼處理它時,REPL 或 IDE 會印出一段 回溯(traceback) -- 它記錄了從最外層指令碼一路往下到拋出例外那一行的呼叫鏈。
回溯由下往上閱讀:
最底下那一行指出例外類別及其訊息(
ValueError: invalid literal for int()...)。其上每一個
File "...", line N, in <name>區塊都是一個 框架(frame) -- 往上每一層就代表更深一層的呼叫。最頂端的框架是指令碼開始之處;最底端的框架則是錯誤發生之處。
先讀最底下,了解 發生了什麼 問題,再往上走,看看指令碼是 如何 走到那裡的。行號會指向指令碼中確切的原始碼位置。
2.41.2. 印出除錯(print debugging)¶
找出指令碼正在做什麼最快的方法,就是把可疑的值印出來。有三個內建函式能讓 print 更有用:
repr()-- 回傳某個值的開發者風格字串。print(repr(value))能區分"5"與5、以及None與"None",而單純的print()做不到。type()-- 回傳某個值的類別。print(type(value))是用來查明那個「應該是 int」的變數是不是其實偷偷是字串的方法。len()-- 序列或集合的長度。出乎意料地有很大一部分的 bug 是差一錯誤或大小不符的問題。
print("got:", repr(value), "type:", type(value), "len:", len(value))
在你關心的每個分支裡都放一個 print -- if 的兩個分支、每個 except 區塊、以及你懷疑跑了零次的迴圈主體。代價只是一行輸出;而其價值在於查明你 以為 正在執行的程式碼路徑是否就是實際執行的那一條。
2.41.3. 探索一個物件¶
有兩個內建函式能回答「我能用這個東西做什麼」:
dir()-- 回傳某物件上定義的所有名稱清單:方法、屬性、雙底線方法(dunder),全部都有。help()-- 印出函式、方法或類別的說明文件字串(docstring,在 CPython 上還會印出簽章)。
把它們搭配使用:dir 找出名稱,help 解釋它的用途。
2.41.3.1. 用 dir 找名稱¶
>>> dir([1, 2, 3])
['__add__', '__class__', '__contains__', '__delitem__',
'__eq__', '__ge__', ..., 'append', 'clear', 'copy',
'count', 'extend', 'index', 'insert', 'pop', 'remove',
'reverse', 'sort']
清單的第一段是繼承自所有物件的雙底線方法(dunder);真正值得掃描的名稱通常在它們後面。dir 可用於任何東西 -- 類別、實例、模組、內建型別:
>>> import json
>>> dir(json)
['__name__', 'dump', 'dumps', 'load', 'loads']
那第二種形式,正是不離開 REPL 就能查明某個模組實際公開了哪些頂層名稱的方法。
2.41.3.2. 用 help 查詢¶
一旦 dir 浮現出候選名稱,help 便會描述它:
>>> help(str.split)
split(sep=None, maxsplit=-1)
Return a list of the words in the string, ...
在 MicroPython 上,help 比 CPython 上精簡 -- 有時只有簽章、有時只有一行說明文件字串、對內建 C 函式有時則什麼都沒有。即便如此,當 IDE 的工具提示不在手邊時,它仍是一個快速的提醒。
2.41.4. 當程式卡住時¶
不會回傳的指令碼比拋出例外的指令碼更難診斷。常見的元凶有:
條件永遠不會變成假的
while迴圈。在每次迭代時印出迴圈變數;如果該值沒有變化,代表迴圈主體有 bug。一個在等待永遠不會到來的輸入的阻塞呼叫 -- 例如從空佇列讀取、或一個沒有結束的 sleep。在該呼叫前後夾上 print,看看指令碼卡在哪一行。
無窮遞迴。它最終觸發時的回溯(伴隨
RecursionError)通常會直接指向它。
對於卡住的指令碼,最有效的恢復方式是 IDE 的 stop 按鈕,它會透過 USB 對指令碼送出一個 KeyboardInterrupt。這個中斷會在目前正在執行的那一行浮現為一段回溯 -- 那往往正是沒有回傳的那一行。
備註
如果某個卡住的情況抵抗了一切診斷 -- 指令碼看起來正確、中斷回溯指向某個內建函式或韌體程式碼而非你的指令碼、或同樣的程式碼在先前的韌體版本上能正常運作 -- 那原因可能是韌體的 bug,而非指令碼的 bug。把指令碼縮減到仍會卡住的最小可重現範例,並在 OpenMV 論壇 上提交一份報告。請附上韌體版本、執行它的板子,以及縮減後的指令碼。
2.41.5. 出貨前把診斷程式碼移除¶
開發期間策略性地使用 print 很棒;但留在正式版指令碼裡的上百個 print 呼叫會弄亂輸出,並佔用本可供實際工作使用的堆積(heap)。當 bug 修好後,把這些 print 移除(或用一個可以關掉的除錯旗標把它們守護起來)。
對於應該長期留在程式碼路徑中的診斷,請從 print() 改用 logging 模組。它會為每則訊息附上一個 等級(debug、info、warning、error),並讓單一設定就能在正式版中讓那些安靜的訊息靜默:
import logging
log = logging.getLogger("main")
log.info("starting up")
log.debug("loaded config: %s", config)
log.warning("falling back to defaults")
把記錄器的等級設為 logging.WARNING,會讓 info 和 debug 呼叫的成本幾乎為零(訊息字串根本不會被建立),而無須把那些行註解掉。這使得 logging 成為 永久性 診斷的正確工具;至於 用完即丟 的診斷,原始的 print 就夠了。