您好,登錄后才能下訂單哦!
這篇文章主要介紹“怎么在Python調試過程中設置不中斷的斷點”,在日常操作中,相信很多人在怎么在Python調試過程中設置不中斷的斷點問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”怎么在Python調試過程中設置不中斷的斷點”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
在諸多可選的 Python 調試器中,使用最廣泛的三個是:
pdb,它是 Python 標準庫的一部分
PyDev,它是內嵌在 Eclipse 和 Pycharm 等 IDE 中的調試器
ipdb,它是 IPython 的調試器
Python 調試器的選擇雖多,但它們幾乎都基于同一個函數:sys.settrace
。 值得一提的是, sys.settrace 可能也是 Python 標準庫中最復雜的函數。
簡單來講,settrace
的作用是為解釋器注冊一個跟蹤函數,它在下列四種情形發生時被調用:
函數調用
語句執行
函數返回
異常拋出
一個簡單的跟蹤函數看上去大概是這樣:
def simple_tracer(frame, event, arg): co = frame.f_code func_name = co.co_name line_no = frame.f_lineno print("{e} {f} {l}".format(e=event, f=func_name, l=line_no)) return simple_tracer
在分析函數時我們首先關注的是參數和返回值,該跟蹤函數的參數分別是:
frame
,當前堆棧幀,它是包含當前函數執行時解釋器里完整狀態的對象
event
,事件,它是一個值可能為 call
、line
、return
或 exception
的字符串
arg
,參數,它的取值基于 event
的類型,是一個可選項
該跟蹤函數的返回值是它自身,這是由于解釋器需要持續跟蹤兩類跟蹤函數:
全局跟蹤函數(每線程):該跟蹤函數由當前線程調用 sys.settrace
來設置,并在解釋器創建一個新的堆棧幀時被調用(即代碼中發生函數調用時)。雖然沒有現成的方式來為不同的線程設置跟蹤函數,但你可以調用 threading.settrace
來為所有新創建的 threading
模塊線程設置跟蹤函數。
局部跟蹤函數(每一幀):解釋器將該跟蹤函數的值設置為全局跟蹤函數創建幀時的返回值。同樣也沒有現成的方法能夠在幀被創建時自動設置局部跟蹤函數。
該機制的目的是讓調試器對被跟蹤的幀有更精確的把握,以減少對性能的影響。
僅僅依靠上文提到的內容,用自制的跟蹤函數來構建一個真正的調試器似乎有些不切實際。幸運的是,Python 的標準調試器 pdb 是基于 Bdb 構建的,后者是 Python 標準庫中專門用于構建調試器的基類。
基于 Bdb 的簡易斷點調試器看上去是這樣的:
import bdbimport inspect class Debugger(bdb.Bdb): def __init__(self): Bdb.__init__(self) self.breakpoints = dict() self.set_trace() def set_breakpoint(self, filename, lineno, method): self.set_break(filename, lineno) try : self.breakpoints[(filename, lineno)].add(method) except KeyError: self.breakpoints[(filename, lineno)] = [method] def user_line(self, frame): if not self.break_here(frame): return # Get filename and lineno from frame (filename, lineno, _, _, _) = inspect.getframeinfo(frame) methods = self.breakpoints[(filename, lineno)] for method in methods: method(frame)
這個調試器類的全部構成是:
繼承 Bdb
,定義一個簡單的構造函數來初始化基類,并開始跟蹤。
添加 set_breakpoint
方法,它使用 Bdb
來設置斷點,并跟蹤這些斷點。
重載 Bdb
在當前用戶行調用的 user_line
方法,該方法一定被一個斷點調用,之后獲取該斷點的源位置,并調用已注冊的斷點。
Rookout 的目標是在生產級性能的使用場景下提供接近普通調試器的使用體驗。那么,讓我們來看看先前構建出來的簡易調試器表現的如何。
為了衡量調試器的整體性能開銷,我們使用如下兩個簡單的函數來進行測試,它們分別在不同的情景下執行了 1600 萬次。請注意,在所有情景下斷點都不會被執行。
def empty_method(): pass def simple_method(): a = 1 b = 2 c = 3 d = 4 e = 5 f = 6 g = 7 h = 8 i = 9 j = 10
在使用調試器的情況下需要大量的時間才能完成測試。糟糕的結果指明了,這個簡陋 Bdb
調試器的性能還遠不足以在生產環境中使用。
降低調試器的額外開銷主要有三種方法:
盡可能的限制局部跟蹤:由于每一行代碼都可能包含大量事件,局部跟蹤比全局跟蹤的開銷要大得多。
優化 call
事件并盡快將控制權還給解釋器:在 call
事件發生時調試器的主要工作是判斷是否需要對該事件進行跟蹤。
優化 line
事件并盡快將控制權還給解釋器:在 line
事件發生時調試器的主要工作是判斷我們在此處是否需要設置一個斷點。
于是我們復刻了 Bdb
項目,精簡特征、簡化代碼,針對使用場景進行優化。這些工作雖然得到了一些效果,但仍無法滿足我們的需求。因此我們又繼續進行了其它的嘗試,將代碼優化并遷移至 .pyx
使用 Cython 進行編譯,可惜結果(如下圖所示)依舊不夠理想。最終,我們在深入了解 CPython 源碼之后意識到,讓跟蹤過程快到滿足生產需求是不可能的。
熬過先前對標準調試方法進行的試驗-失敗-再試驗循環所帶來的失望,我們將目光轉向另一種選擇:字節碼操作。
Python 解釋器的工作主要分為兩個階段:
將 Python 源碼編譯成 Python 字節碼:這種(對人類而言)不可讀的格式專為執行的效率而優化,它們通常緩存在我們熟知的 .pyc
文件當中。
遍歷 解釋器循環中的字節碼: 在這一步中解釋器會逐條的執行指令。
我們選擇的模式是:使用字節碼操作來設置沒有全局額外開銷的不中斷斷點。這種方式的實現首先需要在內存中的字節碼里找到我們感興趣的部分,然后在該部分的相關機器指令前插入一個函數調用。如此一來,解釋器無需任何額外的工作即可實現我們的不中斷斷點。
這種方法并不依靠魔法來實現,讓我們簡要地舉個例子。
首先定義一個簡單的函數:
def multiply(a, b): result = a * b return result
在 inspect 模塊(其包含了許多實用的單元)的文檔里,我們得知可以通過訪問 multiply.func_code.co_code
來獲取函數的字節碼:
'|\x00\x00|\x01\x00\x14}\x02\x00|\x02\x00S'
使用 Python 標準庫中的 dis 模塊可以翻譯這些不可讀的字符串。調用 dis.dis(multiply.func_code.co_code)
之后,我們就可以得到:
4 0 LOAD_FAST 0 (a) 3 LOAD_FAST 1 (b) 6 BINARY_MULTIPLY 7 STORE_FAST 2 (result) 5 10 LOAD_FAST 2 (result) 13 RETURN_VALUE
到此,關于“怎么在Python調試過程中設置不中斷的斷點”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。