程式執行時,若發生錯誤該怎麼辨?許多時侯,我們都假設程式不應該出錯。但,出錯是在所難免,往往有當初不預期的狀況出現。因此,保留適當的訊息變的非常重要。這些訊息,是 programmer 解決問題的重要依據。尤其網路的 service ,通常在遠端的機器上執行,若不保留足夠的資訊,往往無法重建錯誤發生當時的狀況。因此, error message 的 log 就變的非常重要。
Python 對錯誤的反應,往往是產生 exception 。在 exception 發生時,會將 exception 的內容輸出到 stderr 。似乎只需將這些訊息保留下來, programmer 就能據以追蹤問題發生的原因。但事實上,以預設的錯誤訊息進行除錯,常常很沒有效率。
Traceback (most recent call last):
File "www/cgi/new.py", line 51, in <module>
skel(new_log)
File "www/cgi/new.py", line 44, in skel
raise '....'
Python 預設的錯誤訊息只有輸出 exception 發生時,程式呼叫的次序,也就是 traceback (上例)。 traceback 只能指出錯誤發生的位置,卻沒有錯誤發生時的變數內容。在不知道變數內容的狀況下,有時我們無法理解程式為何會出錯,而產生疑惑,使的除錯更加困難。這樣的狀況並不少見,因此我很難加以忽視。例如,當我們透過 DB-API 執行 SQL command 時,錯誤原因的判定往往有賴於 command 的參數內容。例如:
001 cx = sqlite3.connect("test.db")
002 cur = cx.cursor()
003 cur.execute('select * from uid = ?', (uid,))
當第三行執行時,
Python 通常只會告訴你錯誤發生在該行。然而,單就該行 query ,實在難以斷定錯誤的原因。很有可能
database 裡的 uid 欄位是整數,而傳入的參數卻是字串,因而造成錯誤。這時若能保留 uid 變數的內容,將能很快速的界定錯誤的起因。
捕捉
因此,我們應該改寫
Python 處理 exception 方式,保留錯誤發生時的變數內容。要做到這一點,最簡單的方式就是在程式的進入點,以 try...except... 捕捉所有的錯誤。如下例:
001 if __name__ == '__main__':
002 try:
003 main()
004 except:
005 # exception handler
006 pass
當我們捕捉到錯誤時,我們可以透過 sys module 下的 exc_info() 取得 traceback 的內容,並據以取得所需的資訊。
001 import sys
002 tp, val, tb = sys.exc_info()
- tp exception 的 type
- val 是 raise 時,第二個參數
- tb 則保留了 exception 發生時的呼叫狀態和堆疊內容
例如
001 raise ValueError, 'invalid argument'
捕捉到的內容為
- tp = ValueError
- val = ValueError('invalid argument')
於是,我們就可以用下面的程式輸出錯誤內容
001 import sys
002 tp, val, tb = sys.exc_info()
003 print >> sys.stderr, '%s: %s' % (str(tp), str(val)
traceback
traceback 保留程式執行當中,呼叫的次序,和 frame 的內容。透過 traceback ,我們能取得程式呼叫的流程,呼叫的 function 名稱,呼叫者的檔案名稱和行數,甚至是每一個 function 的 global 和 local namespace 的內容。exc_info() 傳回的 traceback 保存的是最外層 function call 的 frame , 也就是程式進入點所在的 module 。透過 traceback.tb_next 可以取得往內一層的 traceback ,可以取得另一個 frame 。所謂的內、外層是以程式呼叫的次序決定。 程式呼叫 A() , A() 呼叫 B() ,B() 再呼叫
C() 時,呼叫 A() 的程式是最外層,然後是 A() , 最內層是
C() 。每一次 function 呼叫時,都會建立一個 frame ,以儲存 function 執行時的狀態。因此,這時系統的狀態如下圖:
exc_info() 傳回的是最外層,也就是右邊最上方的 traceback。透過該 traceback ,我們能取得每一層呼叫的 frame 。 frame 記錄執行的 function ,執行的位置,和執行時的 local 和 global namespace 。
function 資訊
透過 frame 的 f_code 屬性,能取得 function 的相關資訊。例如,
- frame.f_code.co_name 記錄 function 或 module 名稱
- frame.f_code.co_filename 記錄 function 所在的檔案
變數內容
透過 frame 的 f_locals 和 f_globals 可以取得 frame 執行時的 local 和 global namespace ,兩者皆為 dictionary 。透過該 dictionary ,我們可以取得所有變數的名稱和變數的值。例如,下面的 code 印出所有變數的內容。
001 import sys
002 tp, val, tb = sys.exc_info()
003 while tb:
004 frame = tb.tb_frame
005 print >> sys.stderr, 'locals = %s' % (repr(frame.f_locals))
006 print >> sys.stderr, 'globals = %s' % (repr(frame.f_globals))
007 print >> sys.stderr
008 tb = tb.tb_next
009 pass
透過 traceback 的 tb_next 屬性,我們能一層接著一層,由外往內取得所有的變數內容。
excepthook
前面我們在程式的進入點,透過 try...except.... 語法,捕捉所有漏網的 exception 。這是一種方法,但如果你的系統有許多隻程式組成時,就必需不斷重複這些 code 。另一種方式是設定 excepthook ,任何未被捕獲的 exception ,最後就會導致程式結束。在這種情況下,程式結束之前會先呼叫 excepthook,我們可以設定 excepthook ,以改變錯誤訊息內容。如下:
001 import sys
002 def myhook(tp, val, tb):
003 # .......
004 sys.excepthook = myhook
hook 的三個參數,正是 exc_info() 傳回的三個變數。
結論
程式難免會有意料之外的錯誤,最差的狀況就是把所有的錯誤隱藏起來。將錯誤忠實的記錄下,還是比較好的策略。透過將 exception 的發生,將 frame 次序,和變數的內容記錄下來,能很有效的改善除錯效率。據我過去的經驗,通常我們只需記錄最內層 frame 的 local namespace 就足夠了。但,如果空間不是問題,將所有 frame 的資訊記錄下來,那就更萬無一失了。