偵錯工具
在撰寫 Python 程式的時候,如果想要知道某個變數的值或是函數執行的結果,常會使用 print()
函數來印出點東西來看看。這樣雖然簡單、直覺,但當專案變複雜的時候,只靠 print()
函數抓問題的效率不太夠。Python 有一個內建的偵錯器叫做 pbd
,可以幫助我們更有效率地找出問題。
什麼是偵錯器
偵錯器(Debugger)是一種用來找出程式碼中錯誤的工具。當我們的程式碼出現問題時,我們可以透過偵錯器在程式碼的特定位置設置「中斷點(Breakpoint)」,當 Python 程式走到中斷點時,程式就像被按暫停一樣,然後我們趁這個機會做我們想做的事...我是指檢查變數的值、逐步執行程式碼、找出問題所在。
使用 Pdb 偵錯器
先上一段程式碼:
import math
square = lambda n: math.pow(n, 2)
def calc_numbers(numbers, fn):
data = []
for n in numbers:
data.append(fn(n))
return data
numbers = [1, 4, 5, 0, 9, 5, 2, 7]
print(calc_numbers(numbers, square))
如果各位在基礎篇都學的不錯的話,上面這段程式碼應該難不倒大家,簡單的說就是把某個串列跟函數傳給 calc_numbers()
函數,最後得到一新的串列。
在之前如果我們想要知道 calc_numbers()
函數到底接了什麼參數,或是串列 data
的值是什麼,我們可能會使用 print()
函數:
def calc_numbers(numbers, fn):
data = []
print(data, members) # <-- 在這裡印出來
for n in numbers:
# ... 略...
如果使用 Debugger 的話,可以在想要停下來的地方加上 breakpoint()
函數,像這樣:
def calc_numbers(numbers, fn):
data = []
breakpoint() # <-- 在這裡設定中斷點
for n in numbers:
data.append(fn(n))
return data
這樣待會執行到這行程式的時候就會停下來,像這樣:
$ python demo.py
> /private/tmp/demo.py(8)calc_numbers()
-> for n in numbers:
(Pdb)
這裡看到的 demo.py(8)
表示目前程式停在 demo.py
這個檔案的第 7 行,然後即將執行第 8 行的 for
迴圈,而在前面出現的 (Pdb)
提示字元表示目前我們正在 Debugger 的環境裡。先使用 h
或 help
指令看看在 Pdb 裡有哪些指令可以用:
(Pdb) h
Documented commands (type help <topic>):
========================================
EOF c d h list q rv undisplay
a cl debug help ll quit s unt
alias clear disable ignore longlist r source until
args commands display interact n restart step up
b condition down j next return tbreak w
break cont enable jump p retval u whatis
bt continue exit l pp run unalias where
雖然指令看起來好像有點多,但有些指令是同一個,只是完整跟簡寫的差別而已,而且其實常用的指令就只有那幾個...
觀察狀態
我們先用最簡單的 p
指令來印點東西:
(Pdb) p data
[]
(Pdb) p fn
<function <lambda> at 0x1008162a0>
p
指令就是 print
的意思,表示可以把某個值印出來看看。可以看到在這個當下,串列 data
才剛剛被初始化,所以還是空的。使用 l
或 list
指令,可以看到目前所在位置附近的程式碼:
(Pdb) l
3 square = lambda n: math.pow(n, 2)
4
5 def calc_numbers(numbers, fn):
6 data = []
7 breakpoint()
8 -> for n in numbers:
9 data.append(fn(n))
10
11 return data
12
13
(Pdb) l
14 numbers = [1, 4, 5, 0, 9, 5, 2, 7]
15 print(calc_numbers(numbers, square))
[EOF]
l
指令會印出目前所在位置的前後幾行程式碼,如果再執行一次會再往下繼續印,最後一行的 [EOF]
表示已經到底了(End Of File)。如果想再回去看剛剛的程式碼,使用 l
指令的時候加上行號,例如 l 10
就會印出第 10 行附近的程式碼,除了行號也可以使用 .
印出目前所在位置的程式碼。
另一個跟 l
有點像的,是 ll
或是 longlist
指令,這會印出整個函數的程式碼:
(Pdb) ll
5 def calc_numbers(numbers, fn):
6 data = []
7 breakpoint()
8 -> for n in numbers:
9 data.append(fn(n))
10
11 return data
執行程式
Pdb 不是只能讓我們觀察狀態,我們也可以用它來執行程式碼。透過 n
或 next
指令,可以執行下一行程式碼:
(Pdb) n
> /private/tmp/demo.py(9)calc_numbers()
-> data.append(fn(n))
這表示我們剛剛執行了第 8 行的 for
迴圈,接下來準備執行第 9 行的程式碼。這時可再使用 l
指令檢視一下:
(Pdb) l
4
5 def calc_numbers(numbers, fn):
6 data = []
7 breakpoint()
8 for n in numbers:
9 -> data.append(fn(n))
10
11 return data
12
13
14 numbers = [1, 4, 5, 0, 9, 5, 2, 7]
前面的箭頭表示準備執行第 9 行,猜猜看如果印出 data
會看到什麼:
(Pdb) p data
[]
這時候的 data
是空的,因為我們還沒有執行 data.append(fn(n))
這行程式碼。如果這時再執行一次 n
指令,會進到下一個迴圈,data
的值就會變的不一樣了:
(Pdb) n
> /private/tmp/demo.py(8)calc_numbers()
-> for n in numbers:
(Pdb) p data
[1.0]
可以看到目前 data
裡面只有一個元素,這是因為我們剛剛執行了 data.append(fn(n))
這行程式碼,所以 data
裡面有了一個元素。
除了看到目前所在位置可以檢視的變數外,我們也可使用 u
或 up
指令,往上一層看看:
(Pdb) u
> /private/tmp/demo.py(15)<module>()
-> print(calc_numbers(numbers, square))
(Pdb) p data
*** NameError: name 'data' is not defined
因為現在往上一層了,對外面來說 data
變數是不存在的。可以往上也就可以往下一層,是使 用 d
或 down
指令:
(Pdb) d
> /private/tmp/demo.py(8)calc_numbers()
-> for n in numbers:
(Pdb) p data
[1.0]
這樣就又回到原本的位置了。剛才的 n
指令是執行下一行程式碼,如果我想要看看那個 fn(n)
,也就是傳進去的 square
函數在什麼的話,可使用 s
或 step
指令:
(Pdb) s
--Call--
> /private/tmp/demo.py(3)<lambda>()
-> square = lambda n: math.pow(n, 2)
可以看到現在箭頭走到 square
這個函數裡了,接著可以繼續 n
:
(Pdb) n
> /private/tmp/demo.py(3)<lambda>()
-> square = lambda n: math.pow(n, 2)
(Pdb) n
--Return--
> /private/tmp/demo.py(3)<lambda>()->16.0
-> square = lambda n: math.pow(n, 2)
(Pdb) n
> /private/tmp/demo.py(8)calc_numbers()
-> for n in numbers:
n
幾次之後,會看到 --Return--
,這表示我們準備要已經離開了 square
函數並且有回傳值 2
,再繼續 n
就會回到原本的位置。
因為這裡是一個 for
迴圈,所以執行 n
的時候會一直在迴圈裡轉啊轉的,直到轉完為止。如果我想要快一點跳到某一行,可使用 until
指令並且在後面加上行數:
(Pdb) until 11
> /private/tmp/demo.py(11)calc_numbers()
-> return data
(Pdb) p data
[1.0, 16.0, 25.0, 0.0, 81.0, 25.0, 4.0, 49.0]
這樣 Pdb 會執行到行數大於或等於第 11 行的程式碼,這樣就可以結束這個迴圈,這時候也可以看到 data
串列裡的值也都算完了。
另外兩個常用來控制流程的指令,一個是 c
或 continue
,這個指令會讓程式一直執行到下一個中斷點,不過因為我們的範例裡只有一個中斷點,所以會直接執行到程式結束為止。另一個指令是 q
或是 quit
,這個指令會直接讓 Pdb 結束並且離開程式。