使用pdb進行Python除錯

匡吉發表於2021-06-30

除錯應用有時是一個不受歡迎的工作,當你長期編碼之後,只希望寫的程式碼順利執行。但是,很多情況下,我們需要學習一個新的語言功能或者實驗檢測新的方法,從而去理解其中執行的機制原理。

即使不考慮這樣的場景,除錯程式碼仍然是有必要的,所以學會在工作中使用偵錯程式是很重要的。本篇教程中,我將會給出基本的使用關於pdb----Python‘s interative source code debugger。

首先給出一些pdb的基礎知識,大家可以儲存這篇文章方便後續的閱讀。pdb類似於其他的偵錯程式,是獨立的工具,當你需要一個偵錯程式時,它們是不可替代的。本篇教程的最後,大家將會學習到如何使用偵錯程式來檢視應用中的任何變數,可以在任何時刻停止或恢復應用執行流程,從而瞭解到每行程式碼是如何影響應用的內部狀態的。

這有助於追蹤難以發現的bug,並且實現快速可靠的解決缺陷程式碼。有時候,在pdb中單步除錯程式碼,然後檢視變數值的變化,可以有助於我們對應用程式碼的深層次理解。pdb是Python標準庫的一部分,所以只要我們使用了Python直譯器,也就可以使用pdb,非常方便。

本文所使用的例項程式碼會放在文章的末尾,使用的環境是Python3.6及以後的直譯器,大家可以下載原始碼方便學習。

1.開始階段:答應一個變數值

開始案例,我們首先探索pdb最簡單的使用:查閱一個變數的值。首先,我們在一個原始碼檔案中某一行,寫入下列語句:

import pdb; pdb.set_trace()

當這一行程式碼被執行後,Python程式碼檔案就會被暫停執行,等待你釋出命令來指導它下一步如何操作。執行上面的程式碼後,你可以在命令列介面看到(Pdb)的提示符,這就意味著,程式碼被停止了,等待命令的輸入。

自從Python3.7以來,官方標準建議使用標準庫內部函式breakpoint()替代上面的程式碼(注意:本文附帶的程式碼都使用的上面程式碼形式),這樣可以更加加快直譯器的執行和除錯:

breakpoint()

預設情況下,breakpoint()將會倒入pdb模組,然後呼叫pdb.set_trace()函式,只是它進行了進一步封裝。但是,使用breakpoint()可以更加靈活,並且允許使用者通過呼叫它的API來控制除錯行為,以及使用環境變數PYTHONBREAKPOINT。例如,當我們設定PYTHONBREAKPOINT=0在我們的環境中,這就會完全關閉breakpoint()的功能,從而關閉除錯功能。

此外,我們還可以不用手動在原始碼檔案中加入斷點程式碼,只需要在命令列輸入執行指令時通過引數傳遞來設定,例如:

$ python3 -m pdb app.py arg1 arg2

那麼,讓我們直接進入本小節的內容,也就是查閱程式碼中變數的值,看下面的案例,使用的原始碼檔案是codeExample1.py:

#!/usr/bin/env python3

filename = __file__
import pdb; pdb.set_trace()
print(f'path={filename}')

而在你命令列介面,執行上述的Python程式碼,你就可以得到下面的輸出結果:

$ ./codeExample1.py
> /code/codeExample1.py(5)<module>()
-> print(f'path={filename}')
(Pdb)

接下來,讓我們輸入p filename這個命令,來檢視filename這個變數的值,可以看到下述結果:

(Pdb)	p filename
'./codeExample1.py'
(Pdb)

因為我們使用的是一個命令列介面介面程式(command-line interface),那麼注意一下輸出的字元和格式,解釋如下:

  • >開始的第一行告訴我們所執行的原始碼檔名稱,原始碼名稱之後,用小括號包含的是當前程式碼行數,再後面就是函式名稱。這裡,因為我們沒用呼叫任何函式,而是處於模組級別,所用見到的是()。
  • ->開始的第二行表示目前行數程式碼對應的具體程式碼內容。
  • (Pdb)是一個pdb提示符,等待下一個命令的輸入。

我們可以使用q命令,表示推出除錯(quit)。

2.列印表示式

當使用命令p,我們同樣可以輸入一個表示式,讓Python來計算表示式的值。 如果傳入一個變數名,pdb就會答應當前變數對應的值。但是,我們可以進一步調查我們應用程式當前的執行狀態。

下面案例中,當函式get_path()被呼叫,為了檢視在這個函式中發生了什麼,我已經提前插入斷點程式pdb.set_trace(),來阻斷程式的執行,原始碼codeExample2.py內容如下:

#!/usr/bin/env python3

import os


def get_path(filename):
    """Return file's path or empty string if no path."""
    head, tail = os.path.split(filename)
    import pdb; pdb.set_trace()
    return head


filename = __file__
print(f'path = {get_path(filename)}')

如果在命令列中執行這個檔案,可以得到下面的輸出:

$ ./codeExample2.py 
> /code/example2.py(10)get_path()
-> return head
(Pdb) 

那麼此時,我們處於什麼階段:

  • >:表示我們在原始碼檔案codeExample2.py的第10行程式碼處,此處是函式get_path()。如果執行命令p輸出的標量,就是當前所引用的程式碼幀,也就是當前上下文內的標量。
  • ->:執行的程式碼已經被停止在return head處,這一行程式碼還沒有被執行,是位於codeExample2.py檔案的第10行處,具體是在函式get_path()內。

如果想要檢視應用當前狀態程式碼上下文情況,可以使用命令ll(longlist),來檢視程式碼內容,具體內容如下:

(Pdb) ll
  6     def get_path(filename):
  7         """Return file's path or empty string if no path."""
  8         head, tail = os.path.split(filename)
  9         import pdb; pdb.set_trace()
 10  ->     return head
(Pdb) p filename
'./codeExample2.py'
(Pdb) p head, tail
('.', 'codeExample2.py')
(Pdb) p 'filename: ' + filename
'filename: ./codeExample2.py'
(Pdb) p get_path
<function get_path at 0x100760e18>
(Pdb) p getattr(get_path, '__doc__')
"Return file's path or empty string if no path."
(Pdb) p [os.path.split(p)[1] for p in os.path.sys.path]
['pdb-basics', 'python36.zip', 'python3.6', 'lib-dynload', 'site-packages']
(Pdb) 

你可以輸入任何有效的Python表示式接在p後面,來進行計算。當你正在除錯並希望在執行時直接在應用程式中測試替代實現時,這尤其有用。您還可以使用命令 pp(漂亮列印)來美觀列印表示式。 如果您想列印具有大量輸出的變數或表示式,例如列表和字典。 如果可以,漂亮列印將物件保留在一行上,如果它們不適合允許的寬度,則將它們分成多行。

3.除錯程式碼

在這一部分,我們主要是用兩個命令來實現程式碼的除錯,如下圖所示:

命令n(next)和s(step)的區別是,pdb執行停止的位置。使用n(next),pdb會進行執行,直到執行到當前函式或模組的下一行程式碼,即:如果有外部函式被呼叫,不會跳轉到外部函式程式碼中,可以把n理解為“step over”。使用s(step)來執行當前程式碼,但是如果有外部函式被呼叫,會跳轉到外部函式中,可以理解為“step into”,如果執行到外部跳轉函式,s命令會輸出--Call--。

n(next)和s(step)命令都會暫定程式碼的執行,當執行到當前函式的結束部分,並且列印出--Return--,下面是codeExample3.py檔案原始碼內容:

#!/usr/bin/env python3

import os


def get_path(filename):
    """Return file's path or empty string if no path."""
    head, tail = os.path.split(filename)
    return head


filename = __file__
import pdb; pdb.set_trace()
filename_path = get_path(filename)
print(f'path = {filename_path}')

如果在命令列中執行這個檔案,同時輸入命令n,可以得到下面的輸出:

$ ./codeExample3.py 
> /code/example3.py(14)<module>()
-> filename_path = get_path(filename)
(Pdb) n
> /code/example3.py(15)<module>()
-> print(f'path = {filename_path}')
(Pdb) 

通過使用命令n(next),我們停止在15行程式碼,並且我們也只是在此模組中,沒有跳轉到函式get_path()。此處函式表示為(),表明我們目前是處於模組級別而不是任何函式內部。

再讓我們嘗試使用s(step)命令,輸出為:

$ ./codeExample3.py 
> /code/example3.py(14)<module>()
-> filename_path = get_path(filename)
(Pdb) s
--Call--
> /code/example3.py(6)get_path()
-> def get_path(filename):
(Pdb) 

通過使用s(step)命令,我們停止在函式get_path()內部的第6行程式碼處,因為這個函式是在程式碼檔案中第14行處被呼叫的,注意在s命令之後輸出了--Call--,表明是函式呼叫。為了方便,pdb有命令記憶功能,如果我們要除錯很多程式碼,可以輸入EnterEnter鍵,來重複執行命令。

下面是我們混合使用n(next)和s(step)命令的案例,首先輸入s(step),因為我們想要進入函式get_path(),然後通過命令n(next)在區域性除錯程式碼,並且使用EnterEnter鍵,避免重複輸入命令:

$ ./codeExample3.py 
> /code/codeExample3.py(14)<module>()
-> filename_path = get_path(filename)
(Pdb) s
--Call--
> /code/codeExample3.py(6)get_path()
-> def get_path(filename):
(Pdb) n
> /code/codeExample3.py(8)get_path()
-> head, tail = os.path.split(filename)
(Pdb) 
> /code/codeExample3.py(9)get_path()
-> return head
(Pdb) 
--Return--
> /code/codeExample3.py(9)get_path()->'.'
-> return head
(Pdb) 
> /code/codeExample3.py(15)<module>()
-> print(f'path = {filename_path}')
(Pdb) 
path = .
--Return--
> /code/codeExample3.py(15)<module>()->None
-> print(f'path = {filename_path}')
(Pdb) 

注意輸出的--Call--和--Return--,這是pdb輸出的資訊,提示我們除錯過程的狀態資訊,n(next)和s(step)命令都會在函式返回後停止,這也就是我們會看到--Return--資訊的輸出。此外,還要注意->'.'在第一個--Return--輸出上面的結尾處:

--Return--
> /code/example3.py(9)get_path()->'.'
-> return head
(Pdb) 

當pdb停止到一個函式的結尾,但是還沒有執行到return時,pdb同樣會列印return值,在上面例子中就是'.'.

3.1顯示程式碼

不要忘記,我們上面提到了命令ll(longlist:顯示當前函式或幀的原始碼),在我們除錯進入到不熟悉的程式碼上下文中,這個命令非常有效,我們可以列印出整個函式程式碼,顯示樣例如下:

$ ./codeExample3.py 
> /code/codeExample3.py(14)<module>()
-> filename_path = get_path(filename)
(Pdb) s
--Call--
> /code/codeExample3.py(6)get_path()
-> def get_path(filename):
(Pdb) ll
  6  -> def get_path(filename):
  7         """Return file's path or empty string if no path."""
  8         head, tail = os.path.split(filename)
  9         return head
(Pdb) 

如果想要檢視簡短的程式碼片段,我們可以使用命令l(list),不需要輸入引數,它將會列印11行目前程式碼附近的程式碼內容,案例如下:

$ ./codeExample3.py 
> /code/codeExample3.py(14)<module>()
-> filename_path = get_path(filename)
(Pdb) l
  9         return head
 10     
 11     
 12     filename = __file__
 13     import pdb; pdb.set_trace()
 14  -> filename_path = get_path(filename)
 15     print(f'path = {filename_path}')
[EOF]
(Pdb) l
[EOF]
(Pdb) l .
  9         return head
 10     
 11     
 12     filename = __file__
 13     import pdb; pdb.set_trace()
 14  -> filename_path = get_path(filename)
 15     print(f'path = {filename_path}')
[EOF]
(Pdb) 

4.斷點使用

斷點的正確使用,在我們除錯過程中可以節約大量的時間。不需要單步除錯一行行程式碼,而只用簡單地在我們想要查閱的地方設定一個斷點,就可以直接執行到斷點處進行除錯。同樣的,我們可以新增條件,來讓pdb判斷是否需要設定斷點。通常使用命令b(break)來設定一個斷點,我們可以通過指定程式碼行數,或者是想要除錯的函式名稱,語法如下:

b(reak) [ ([filename:]lineno | function) [, condition] ]

如果filename:沒有在程式碼行數之前被指定,那麼預設就是當前程式碼檔案中。注意第二個可選引數是b: condition,這個功能非常強大。假設在一個場景下,我們想要在某種條件成立的情況下設定斷點,如果我們傳入一個表示式座位第二個引數,pdb會在計算改表示式為true的情況下設定斷點,我們會在下面給出案例。下面案例中,使用了一個工具模組util.py,讓我們在函式get_path()中設定一個斷點,下面是程式碼codeExample4.py的內容:

#!/usr/bin/env python3

import util

filename = __file__
import pdb; pdb.set_trace()
filename_path = util.get_path(filename)
print(f'path = {filename_path}')

下面是工具模組util.py檔案內容:

def get_path(filename):
    """Return file's path or empty string if no path."""
    import os
    head, tail = os.path.split(filename)
    return head

首先,讓我們使用原始碼檔名稱和程式碼行數來設定斷點:

$ ./example4.py 
> /code/example4.py(7)<module>()
-> filename_path = util.get_path(filename)
(Pdb) b util:5
Breakpoint 1 at /code/util.py:5
(Pdb) c
> /code/util.py(5)get_path()
-> return head
(Pdb) p filename, head, tail
('./example4.py', '.', 'example4.py')
(Pdb) 

命令c(continue)是實現被斷點停止後繼續執行的命令,下面,讓我們使用函式名來設定斷點:

$ ./codeExample4.py 
> /code/codeExample4.py(7)<module>()
-> filename_path = util.get_path(filename)
(Pdb) b util.get_path
Breakpoint 1 at /code/util.py:1
(Pdb) c
> /code/util.py(3)get_path()
-> import os
(Pdb) p filename
'./codeExample4.py'
(Pdb) 

如果輸入b,並且不帶任何引數,就可以檢視到所有已經設定的斷點資訊:

(Pdb) b
Num Type         Disp Enb   Where
1   breakpoint   keep yes   at /code/util.py:1
(Pdb) 

你可以使用命令 disable bpnumber 和 enable bpnumber 禁用和重新啟用斷點。 bpnumber 是斷點列表第一列 Num 中的斷點編號。 注意 Enb 列的值變化:

(Pdb) disable 1
Disabled breakpoint 1 at /code/util.py:1
(Pdb) b
Num Type         Disp Enb   Where
1   breakpoint   keep no    at /code/util.py:1
(Pdb) enable 1
Enabled breakpoint 1 at /code/util.py:1
(Pdb) b
Num Type         Disp Enb   Where
1   breakpoint   keep yes   at /code/util.py:1
(Pdb) 

為了刪除一個斷點,可以使用命令cl(clear):

cl(ear) filename:lineno
cl(ear) [bpnumber [bpnumber...]]

現在,讓我們嘗試在設定斷點時輸入表示式引數,在當前案例場景下,get_path()函式如果接受到一個相對路徑,即:如果路徑名稱不是以/開始的,那麼就不會設定斷點,具體案例如下:

$ ./codeExample4.py 
> /code/codeExample4.py(7)<module>()
-> filename_path = util.get_path(filename)
(Pdb) b util.get_path, not filename.startswith('/')
Breakpoint 1 at /code/util.py:1
(Pdb) c
> /code/util.py(3)get_path()
-> import os
(Pdb) a
filename = './codeExample4.py'
(Pdb) 

如果建立一個斷點後,繼續輸入c(continue)命令來執行,pdb只會在表示式計算為true的情況下停止執行。命令a(args)會列印出當前函式的傳入引數。

上述案例中,如果你通過函式名稱而不是程式碼行數來設定斷點,注意表示式只會使用當前函式的引數或者全域性變數,不然的話,斷點就會不去計算表示式,而直接停止函式執行。如果,我們還是想用不是當前的函式變數來計算表示式,也就是使用的變數不在當前函式引數列表中,那麼就要指定程式碼行數,案例如下:

$ ./codeExample4.py 
> /code/codeExample4.py(7)<module>()
-> filename_path = util.get_path(filename)
(Pdb) b util:5, not head.startswith('/')
Breakpoint 1 at /code/util.py:5
(Pdb) c
> /code/util.py(5)get_path()
-> return head
(Pdb) p head
'.'
(Pdb) a
filename = './codeExample4.py'
(Pdb) 

5.繼續執行程式碼

目前,我們可以使用n(next)和s(step)命令來除錯檢視程式碼,然後使用命令b(break)和c(continue)來停止或繼續程式碼的執行,這裡還有一個相關的命令:unt(until)。使用unt命令類似於c命令,但是它的執行結果是,下一行比當前程式碼行數大的位置。有時,unt更加方便和便捷使用,讓我們在下面案例中展示,首先給出使用語法:

取決於我們是否輸入程式碼行數引數lineno,unt命令可以按照下面兩種方式執行:

  • 沒有lineno,程式碼可以繼續執行到,下一次行數比當前行數大的位置,這就相似於n(next),也就是類似於執行“step over”的另一種形式。但是,命令n和unt的不同點是,unt只會在下一次行數比當前行數大的位置停止,而n命令會停在下一個邏輯執行行。
  • 具有lineno,程式碼會執行到下一次行數比當前行數大或者相等的位置,這就類似於c(continue)後面帶有一個程式碼函式引數。

上面兩種情況下,unt命令類似於n(next)和s(step)都只會停止在當前幀(或函式)。

當你想繼續執行並在當前原始檔中更遠的地方停止時,請使用 unt。 你可以將其視為 n (next) 和 b (break) 的混合體,具體取決於是否傳遞行號引數。

在下面的示例中,有一個帶有迴圈的函式。 在這裡,我們希望繼續執行程式碼並在迴圈後停止,而不是單步執行迴圈的每次迭代或設定斷點,下面是檔案codeExample4unt.py檔案的內容:

#!/usr/bin/env python3

import os


def get_path(fname):
    """Return file's path or empty string if no path."""
    import pdb; pdb.set_trace()
    head, tail = os.path.split(fname)
    for char in tail:
        pass  # Check filename char
    return head


filename = __file__
filename_path = get_path(filename)
print(f'path = {filename_path}')

以及命令下使用unt輸出如下:

$ ./codeExample4unt.py 
> /code/codeExample4unt.py(9)get_path()
-> head, tail = os.path.split(fname)
(Pdb) ll
  6     def get_path(fname):
  7         """Return file's path or empty string if no path."""
  8         import pdb; pdb.set_trace()
  9  ->     head, tail = os.path.split(fname)
 10         for char in tail:
 11             pass  # Check filename char
 12         return head
(Pdb) unt
> /code/codeExample4unt.py(10)get_path()
-> for char in tail:
(Pdb) 
> /code/codeExample4unt.py(11)get_path()
-> pass  # Check filename char
(Pdb) 
> /code/codeExample4unt.py(12)get_path()
-> return head
(Pdb) p char, tail
('y', 'codeExample4unt.py')

ll命令首先用於列印函式的原始碼,然後是 unt。 pdb 記得上次輸入的命令,所以我只是按 Enter 重複 unt 命令。 這將繼續執行程式碼,直到到達比當前行大的原始碼行。

請注意,在上面的控制檯輸出中,pdb 僅在第 10 行和第 11 行停止了一次。由於使用了 unt,因此僅在迴圈的第一次迭代中停止執行。 但是,迴圈的每次迭代都被執行。 這可以在輸出的最後一行進行驗證。 char 變數的值 'y' 等於 tail 值 'codeExample4unt.py' 中的最後一個字元。

6.顯示錶達式

類似於列印表示式p和pp的功能,我們可以使用dispaly [expression]來告訴pdb來顯示一個表達的值,同樣適用undisplay [expression]來清楚一個表示式顯示,下面使用一些使用語法解釋:

下面是一個案例,程式碼檔案為codeExample4display.py,展示了一個迴圈的使用:

$ ./codeExample4display.py 
> /code/codeExample4display.py(9)get_path()
-> head, tail = os.path.split(fname)
(Pdb) ll
  6     def get_path(fname):
  7         """Return file's path or empty string if no path."""
  8         import pdb; pdb.set_trace()
  9  ->     head, tail = os.path.split(fname)
 10         for char in tail:
 11             pass  # Check filename char
 12         return head
(Pdb) b 11
Breakpoint 1 at /code/codeExample4display.py:11
(Pdb) c
> /code/codeExample4display.py(11)get_path()
-> pass  # Check filename char
(Pdb) display char
display char: 'e'
(Pdb) c
> /code/codeExample4display.py(11)get_path()
-> pass  # Check filename char
display char: 'x'  [old: 'e']
(Pdb) 
> /code/codeExample4display.py(11)get_path()
-> pass  # Check filename char
display char: 'a'  [old: 'x']
(Pdb) 
> /code/codeExample4display.py(11)get_path()
-> pass  # Check filename char
display char: 'm'  [old: 'a']

在上面的輸出中,pdb 自動顯示了 char 變數的值,因為每次遇到斷點時,它的值都會發生變化。 有時這很有幫助並且正是你想要的,但還有另一種使用顯示的方法。

你可以多次輸入 display 以構建表示式監視列表。 這比 p 更容易使用。 新增你感興趣的所有表示式後,只需輸入 display 即可檢視當前值:

$ ./codeExample4display.py 
> /code/codeExample4display.py(9)get_path()
-> head, tail = os.path.split(fname)
(Pdb) ll
  6     def get_path(fname):
  7         """Return file's path or empty string if no path."""
  8         import pdb; pdb.set_trace()
  9  ->     head, tail = os.path.split(fname)
 10         for char in tail:
 11             pass  # Check filename char
 12         return head
(Pdb) b 11
Breakpoint 1 at /code/codeExample4display.py:11
(Pdb) c
> /code/codeExample4display.py(11)get_path()
-> pass  # Check filename char
(Pdb) display char
display char: 'e'
(Pdb) display fname
display fname: './codeExample4display.py'
(Pdb) display head
display head: '.'
(Pdb) display tail
display tail: 'codeExample4display.py'
(Pdb) c
> /code/codeExample4display.py(11)get_path()
-> pass  # Check filename char
display char: 'x'  [old: 'e']
(Pdb) display
Currently displaying:
char: 'x'
fname: './codeExample4display.py'
head: '.'
tail: 'codeExample4display.py'

7.Python呼叫者ID

在最後一節,我們會基於上述學習到的內容,來演示“call ID”的功能使用。下面是案例程式碼codeExample5.py的內容:

#!/usr/bin/env python3

import fileutil


def get_file_info(full_fname):
    file_path = fileutil.get_path(full_fname)
    return file_path


filename = __file__
filename_path = get_file_info(filename)
print(f'path = {filename_path}')

以及工具模組fileutil.py檔案內容:

def get_path(fname):
    """Return file's path or empty string if no path."""
    import os
    import pdb; pdb.set_trace()
    head, tail = os.path.split(fname)
    return head

在這種情況下,假設有一個大型程式碼庫,其中包含一個實用程式模組 get_path() 中的函式,該函式使用無效輸入進行呼叫。 但是,它是從不同包中的許多地方呼叫的。

如何檢視呼叫程式是誰?

使用命令w(where)來列印一個程式碼棧序列,就是從低到上顯示所有堆疊:

$ ./codeExample5.py 
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb) w
  /code/codeExample5.py(12)<module>()
-> filename_path = get_file_info(filename)
  /code/codeExample5.py(7)get_file_info()
-> file_path = fileutil.get_path(full_fname)
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb) 

如果這看起來令人困惑,或者你不確定堆疊跟蹤或幀是什麼,請不要擔心。 我將在下面解釋這些術語。 這並不像聽起來那麼困難。

由於最近的幀在底部,從那裡開始並從下往上閱讀。 檢視以 -> 開頭的行,但跳過第一個例項,因為 pdb.set_trace() 用於在函式 get_path() 中輸入 pdb。 在此示例中,呼叫函式 get_path() 的原始碼行是:

-> file_path = fileutil.get_path(full_fname)

在每個 -> 上面的行包含檔名和程式碼行數(在括號中),以及函式名稱,所以呼叫者就是:

  /code/example5.py(7)get_file_info()
-> file_path = fileutil.get_path(full_fname)

顯然在這個簡單的樣例中,我們展示瞭如何查詢函式呼叫者,但是想象一個大型應用程式,您在其中設定了一個帶有條件的斷點,以確定錯誤輸入值的來源,下面讓我們進一步深入。

什麼是堆疊追蹤和棧幀內容?

堆疊跟蹤只是 Python 為跟蹤函式呼叫而建立的所有幀的列表。 框架是 Python 在呼叫函式時建立並在函式返回時刪除的資料結構。 堆疊只是在任何時間點的幀或函式呼叫的有序列表。 (函式呼叫)堆疊在應用程式的整個生命週期中隨著函式被呼叫然後返回而增長和縮小。列印時,這個有序的幀列表,即堆疊,稱為堆疊跟蹤。 你可以隨時通過輸入命令 w 來檢視它,就像我們在上面找到呼叫者一樣。

當前棧幀什麼意思?

將當前幀視為 pdb 已停止執行的當前函式。 換句話說,當前幀是你的應用程式當前暫停的位置,並用作 pdb 命令(如 p(列印))的“參考幀”。p 和其他命令將在需要時使用當前幀作為上下文。 在 p 的情況下,當前幀將用於查詢和列印變數引用。當 pdb 列印堆疊跟蹤時,箭頭 > 表示當前幀。

怎麼使用和切換棧幀?

你可以使用兩個命令 u(向上)和 d(向下)來更改當前幀。 與 p 結合使用,這允許你在任何幀中呼叫堆疊的任何點檢查應用程式中的變數和狀態。使用語法如下:

讓我們看一個使用 u 和 d 命令的例子。 在這種情況下,我們要檢查codeExample5.py 中函式 get_file_info() 的區域性變數 full_fname。 為此,我們必須使用命令 u 將當前幀向上更改一級:

$ ./codeExample5.py 
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb) w
  /code/codeExample5.py(12)<module>()
-> filename_path = get_file_info(filename)
  /code/codeExample5.py(7)get_file_info()
-> file_path = fileutil.get_path(full_fname)
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb) u
> /code/codeExample5.py(7)get_file_info()
-> file_path = fileutil.get_path(full_fname)
(Pdb) p full_fname
'./codeExample5.py'
(Pdb) d
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb) p fname
'./codeExample5.py'
(Pdb) 

因為是在fileutil.py檔案函式get_path()中設定的斷點程式pdb.set_trace(),所以當前幀被設定在此處,如下面所示:

> /code/fileutil.py(5)get_path()

為了訪問和列印在codeExample5.py檔案函式get_file_info()裡的變數full_fname,命令u實現移動到前一個棧幀:

(Pdb) u
> /code/codeExample5.py(7)get_file_info()
-> file_path = fileutil.get_path(full_fname)

請注意,在上面 u 的輸出中,pdb 在第一行的開頭列印了箭頭 >。 這是 pdb,讓你知道幀已更改,並且此源位置現在是當前幀。 現在可以訪問變數 full_fname。 另外,重要的是要意識到第 2 行以 -> 開頭的原始碼行已被執行。 由於幀被移到堆疊上,fileutil.get_path() 已被呼叫。 使用 u,我們將堆疊向上移動(從某種意義上說,回到過去)到呼叫 fileutil.get_path() 的函式 codeExample5.get_file_info()。

繼續這個例子,在列印 full_fname 之後,使用 d 將當前幀移動到其原始位置,並列印 get_path() 中的區域性變數 fname。如果我們願意,我們可以通過將 count 引數傳遞給 u 或 d 來一次移動多個幀。 例如,我們可以通過輸入 u 2 移動到 codeExample5.py 中的模組級別:

$ ./codeExample5.py 
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb) u 2
> /code/codeExample5.py(12)<module>()
-> filename_path = get_file_info(filename)
(Pdb) p filename
'./codeExample5.py'
(Pdb) 

當你除錯和思考許多不同的事情時,很容易忘記你在哪裡。 請記住,你始終可以使用恰當命名的命令 w (where) 來檢視執行暫停的位置以及當前幀是什麼。

8.總結

本篇教程中,我們主要講解了pdb中一些基本常用的內容:

  • 列印表示式
  • 使用n(next)和s(step)命令除錯程式碼
  • 斷點使用
  • 使用unt(until)來繼續執行程式碼
  • 顯示錶達式
  • 查詢一個函式的呼叫者

最後,希望這篇教程對你帶來幫助,本篇教程中對應的程式碼可以在程式碼倉庫中下載:https://github.com/1311440131/pdb_basic_tutorial

相關文章