偵錯工具
在撰寫 Python 程式的時候,如果想要知道某個變數的值或是函數執行的結果,常會使用 print()
函數來印出點東西來看看。這樣雖然簡單、直覺,但當專案變複雜的時候,只靠 print()
函數抓問題的效率可能不太夠。Python 有一個內建的偵錯器叫做 pdb
,可以幫助我們更有效率地找出問題。
什麼是偵錯器
偵錯器(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>>
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