Python 的偵錯神器 – breakpoint 和 pdb

如果想要開發 200 行程式碼以上,並包含 def 或 class 架構的 Python 程式,breakpoint 和 pdb 絕對是個不可或缺的利器!透過它們,可以讓我們在最短時間抓到並修正 bug。

可能程式初學者還不會意識到這件事的重要性。為什麼呢?狀況是這樣的:

坊間大多課程是以 jupyter notebook 這個 IDE 來讓初學者快速上手 Python。當一般初學者使用 jupyter notebook 做程式練習時,由於程式會把每次分段執行過的變數和值都存在記憶體中,因此初學者往往能輕易地把變數列印出來,觀察是否正確執行。這對於初學者來說,確實能加速熟悉 Python 語法。

然而這樣的開發方式是相當不利於維護和擴展的。想想看,當我們跟著老師的程式範例檔案練習,程式碼上百行之後,每次上下滑、好不容易找到變數名稱,再列印出來找 bug。好不容易將 bug 修正,要從 jupyter notebook 的 .ipynb 檔案轉成方便未來執行的 .py 檔,又發現額外的 bug。這過程是不是很崩潰?

再想想另一個情境:我們在學會用 def 寫程式之後,假設是選股模型的程式,讀取資料寫一個 def、計算邏輯又是一個 def、視覺化輸出結果再來一個 def 等。程式架構慢慢變得複雜了,但在執行的過程出現 bug,推測是藏在讀取資料 def 中,偏偏 def 內的變數是區域變數(local variable),離開了這個 def 就六親不認,直接失效了。難道我們要用 print 來把 for 迴圈內一大堆不需要檢查的資料也印出來嗎?如果我們也想要檢查在那當下的其他變數的值,難道又要重新執行 .py 檔、另外印出來?

其實,善用 breakpoint 的話,就可以讓開發者用更簡單明瞭的方式完成偵錯的過程!

Breakpoint 與 pdb 的功能

在想要設定成檢查中斷點的地方,插入一行 pdb 或 breakpoint,當程式運行到此處就會暫停,並提供幾個輸入操作指令方便檢查,效果等同於 Excel VBA 設定中斷點,但更為彈性且直覺。

其實兩者的功能相同,只是 breakpoint 是在 Python 3.7 之後才新增的內建函數。所以,這裡建議使用 Python 3.7 之後版本的讀者直接使用 breakpoint 即可,不須再多 import 套件。

如下方程式碼,pdb 的用法是把 pdb.set_trace() 放在中斷點的位置。

# Python 3.7 以前:
import pdb
pdb.set_trace()

# Python 3.7 之後:
breakpoint()

breakpoint 實際操作範例

我們先用一個簡單的範例做說明,再接著使用 def 那篇文章的範例,講解一個常見的解 bug 流程。對函數 def 不熟的讀者,可以先閱讀這篇文章:函式 def 把重複的動作包裝起來!

範例一:遞迴函數計算費波納契數列

費波納契數列俗稱兔子數列,數列裡每一個值為前兩個值相加 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, …。

下方是個有 bug 的程式碼:

def calculate_fibonacci(length: int = 0):
  if length in [0, 1]:
    return 1
  return calculate_fibonacci(length – 2) + calculate_fibonacci(length – 1)
fib_list = [calculate_fibonacci(i) for i in range(5)]

直接執行,我們把 fib_list 列印出來結果如下:

[1, 1, 2, 3, 5]

很明顯,最前面少了一個「0」。在費波納契數列的定義裡,第 0 項為 0,第 1 項開始才是 1。這時候,我們應該把中斷點 breakpoint 設在哪裡呢?每個人的做法或許會有些差異,不過這裡筆者會放在 if 之前,如此能確保每一次遞迴呼叫 def 的時候,都能經過 breakpoint 停下來。因此,加入中斷點設計的程式碼如下:

def calculate_fibonacci(length: int = 0):
  breakpoint()
  if length in [0, 1]:
    return 1
  return calculate_fibonacci(length – 2) + calculate_fibonacci(length – 1)
fib_list = [calculate_fibonacci(i) for i in range(5)]

執行這段程式碼,我們會發現程式出現一個「(Pdb)」並且停下來了!

這時候我們可以輸入 p length 把變數 length 印出來,檢查現在 length 是多少。我們在印出來之前,可以預期到這個 length 的值必然為 0,因為我們第一次呼叫 calculate_fibonacci 函數是從 range(5) 之中取出第一個值「0」作為傳入參數傳入的。印出來之後,確實 length 就是 0。但我們接著往下一行瞄一眼,就能看到 bug 了!當 length 為 0 的時候,這個函數應該回傳 0 而非 1。

真相大白!這裡我們就能把函數修正成:

def calculate_fibonacci(length: int = 0):
  if length in [0, 1]:
   
return length
  return calculate_fibonacci(length – 2) + calculate_fibonacci(length – 1)
fib_list = [calculate_fibonacci(i) for i in range(5)]

執行後,fib_list 確實就如預期一樣,答案是 [0, 1, 1, 2, 3]

上面這個例子相對單純,在遞迴的第一個中斷點就找到 bug。就算是不用 breakpoint,在剛才 breakpoint 的位置改成 print(length),思考一下也許還能抓出問題。那如果是下面這個呢?

範例二:讀取價量資料的 def

下方程式碼是函數 def 文章中範例,用來讀取本地端的股票價量資料 csv。

import pandas as pd
from datetime import datetime

def get_price(
 symbol_list: list = [],
 start_date: datetime = datetime(1970,1,1),
 end_date: datetime = datetime(1970,1,1)
):
 price = {}
 for symbol in symbol_list:
  path = f“./price/{symbol}.csv”
  data = pd.read_csv(path, index_col=”time”, parse_dates=True)
  price[symbol] = data.loc[start_date, end_date]

 return price

現在的情境是:我在透過 get_price 讀取資料的時候沒問題,但是把資料餵到其他函數計算技術指標的時候,出現 bug 了!於是我想要回到 get_price 函數裡,新增處理價格資料出現空值 NaN 的部分。

聰明的讀者習慣怎麼做呢?如果是個對 Python 資料處理相當熟練的讀者,應該是不需要 breakpoint 就能精準快速地搞定。但如果對於要用哪個套件的函數進行處理,還不是那麼肯定的讀者,這裡建議可以在把資料讀取進來後,也就是 data 開頭的那行之後,加入 breakpoint。你可以在 Pdb 環境下玩個夠,把 data 整理成你要的格式後,再塞進去 price 這個字典裡。

breakpoint 的小技巧

前面講了常見的用法,這裡補充兩個小技巧:

1. 在檢查複雜運算過程的中斷點,我們往往會在 pdb 的環境下輸入 c 或是 n 來讓程式依照指定的方式往前執行一些些。

 Pdb 指令          功能說明   
n向前執行一行程式碼即停下
c向前執行直到下一個中斷點才停下
p印出指定變數的值。例如p length 為印出 length 值
q離開 pdb

2. 如果是要在指定的條件下才進入中斷點,用一個 if 條件式先做判斷,判斷條件成立了才做中斷檢查。

以上是個非常常見的小技巧,提供給各位讀者參考!


量化通粉絲社群,定期分享實用資源
✅加入LINE匿名群組量化通QuantPass」無壓力討論與分享!
✅追蹤量化通的粉絲專頁量化通QuantPass」即時獲取實用的資源!

程式交易課程推薦
📣 Python 程式交易系列線上課程,手把手開始用程式交易打造自己的被動收入!

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *