溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

怎么在Python調(diào)試過程中設(shè)置不中斷的斷點

發(fā)布時間:2021-11-03 17:27:09 來源:億速云 閱讀:263 作者:iii 欄目:編程語言

這篇文章主要介紹“怎么在Python調(diào)試過程中設(shè)置不中斷的斷點”,在日常操作中,相信很多人在怎么在Python調(diào)試過程中設(shè)置不中斷的斷點問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”怎么在Python調(diào)試過程中設(shè)置不中斷的斷點”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!

Python 調(diào)試器的心臟:sys.set_trace

在諸多可選的 Python 調(diào)試器中,使用最廣泛的三個是:

  • pdb,它是 Python 標(biāo)準(zhǔn)庫的一部分

  • PyDev,它是內(nèi)嵌在 Eclipse 和 Pycharm 等 IDE 中的調(diào)試器

  • ipdb,它是 IPython 的調(diào)試器

Python 調(diào)試器的選擇雖多,但它們幾乎都基于同一個函數(shù):sys.settrace。 值得一提的是, sys.settrace 可能也是 Python 標(biāo)準(zhǔn)庫中最復(fù)雜的函數(shù)。

怎么在Python調(diào)試過程中設(shè)置不中斷的斷點

簡單來講,settrace 的作用是為解釋器注冊一個跟蹤函數(shù),它在下列四種情形發(fā)生時被調(diào)用:

  • 函數(shù)調(diào)用

  • 語句執(zhí)行

  • 函數(shù)返回

  • 異常拋出

一個簡單的跟蹤函數(shù)看上去大概是這樣:

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

在分析函數(shù)時我們首先關(guān)注的是參數(shù)和返回值,該跟蹤函數(shù)的參數(shù)分別是:

  • frame,當(dāng)前堆棧幀,它是包含當(dāng)前函數(shù)執(zhí)行時解釋器里完整狀態(tài)的對象

  • event,事件,它是一個值可能為 call、linereturn 或 exception 的字符串

  • arg,參數(shù),它的取值基于 event 的類型,是一個可選項

該跟蹤函數(shù)的返回值是它自身,這是由于解釋器需要持續(xù)跟蹤兩類跟蹤函數(shù):

  • 全局跟蹤函數(shù)(每線程):該跟蹤函數(shù)由當(dāng)前線程調(diào)用 sys.settrace 來設(shè)置,并在解釋器創(chuàng)建一個新的堆棧幀時被調(diào)用(即代碼中發(fā)生函數(shù)調(diào)用時)。雖然沒有現(xiàn)成的方式來為不同的線程設(shè)置跟蹤函數(shù),但你可以調(diào)用 threading.settrace 來為所有新創(chuàng)建的 threading 模塊線程設(shè)置跟蹤函數(shù)。

  • 局部跟蹤函數(shù)(每一幀):解釋器將該跟蹤函數(shù)的值設(shè)置為全局跟蹤函數(shù)創(chuàng)建幀時的返回值。同樣也沒有現(xiàn)成的方法能夠在幀被創(chuàng)建時自動設(shè)置局部跟蹤函數(shù)。

該機(jī)制的目的是讓調(diào)試器對被跟蹤的幀有更精確的把握,以減少對性能的影響。

簡單三步構(gòu)建調(diào)試器 (我們最初的設(shè)想)

僅僅依靠上文提到的內(nèi)容,用自制的跟蹤函數(shù)來構(gòu)建一個真正的調(diào)試器似乎有些不切實際。幸運(yùn)的是,Python 的標(biāo)準(zhǔn)調(diào)試器 pdb 是基于 Bdb 構(gòu)建的,后者是 Python 標(biāo)準(zhǔn)庫中專門用于構(gòu)建調(diào)試器的基類。

基于 Bdb 的簡易斷點調(diào)試器看上去是這樣的:

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)

這個調(diào)試器類的全部構(gòu)成是:

  1. 繼承 Bdb,定義一個簡單的構(gòu)造函數(shù)來初始化基類,并開始跟蹤。

  2. 添加 set_breakpoint 方法,它使用 Bdb 來設(shè)置斷點,并跟蹤這些斷點。

  3. 重載 Bdb 在當(dāng)前用戶行調(diào)用的 user_line 方法,該方法一定被一個斷點調(diào)用,之后獲取該斷點的源位置,并調(diào)用已注冊的斷點。

這個簡易的 Bdb 調(diào)試器效率如何呢?

Rookout 的目標(biāo)是在生產(chǎn)級性能的使用場景下提供接近普通調(diào)試器的使用體驗。那么,讓我們來看看先前構(gòu)建出來的簡易調(diào)試器表現(xiàn)的如何。

為了衡量調(diào)試器的整體性能開銷,我們使用如下兩個簡單的函數(shù)來進(jìn)行測試,它們分別在不同的情景下執(zhí)行了 1600 萬次。請注意,在所有情景下斷點都不會被執(zhí)行。

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

在使用調(diào)試器的情況下需要大量的時間才能完成測試。糟糕的結(jié)果指明了,這個簡陋 Bdb 調(diào)試器的性能還遠(yuǎn)不足以在生產(chǎn)環(huán)境中使用。

怎么在Python調(diào)試過程中設(shè)置不中斷的斷點

對調(diào)試器進(jìn)行優(yōu)化

降低調(diào)試器的額外開銷主要有三種方法:

  1. 盡可能的限制局部跟蹤:由于每一行代碼都可能包含大量事件,局部跟蹤比全局跟蹤的開銷要大得多。

  2. 優(yōu)化 call 事件并盡快將控制權(quán)還給解釋器:在 call 事件發(fā)生時調(diào)試器的主要工作是判斷是否需要對該事件進(jìn)行跟蹤。

  3. 優(yōu)化 line 事件并盡快將控制權(quán)還給解釋器:在 line 事件發(fā)生時調(diào)試器的主要工作是判斷我們在此處是否需要設(shè)置一個斷點。

于是我們復(fù)刻了 Bdb 項目,精簡特征、簡化代碼,針對使用場景進(jìn)行優(yōu)化。這些工作雖然得到了一些效果,但仍無法滿足我們的需求。因此我們又繼續(xù)進(jìn)行了其它的嘗試,將代碼優(yōu)化并遷移至 .pyx 使用 Cython 進(jìn)行編譯,可惜結(jié)果(如下圖所示)依舊不夠理想。最終,我們在深入了解 CPython 源碼之后意識到,讓跟蹤過程快到滿足生產(chǎn)需求是不可能的。

怎么在Python調(diào)試過程中設(shè)置不中斷的斷點

放棄 Bdb 轉(zhuǎn)而嘗試字節(jié)碼操作

熬過先前對標(biāo)準(zhǔn)調(diào)試方法進(jìn)行的試驗-失敗-再試驗循環(huán)所帶來的失望,我們將目光轉(zhuǎn)向另一種選擇:字節(jié)碼操作。

Python 解釋器的工作主要分為兩個階段:

  1. 將 Python 源碼編譯成 Python 字節(jié)碼:這種(對人類而言)不可讀的格式專為執(zhí)行的效率而優(yōu)化,它們通常緩存在我們熟知的 .pyc 文件當(dāng)中。

  2. 遍歷 解釋器循環(huán)中的字節(jié)碼: 在這一步中解釋器會逐條的執(zhí)行指令。

我們選擇的模式是:使用字節(jié)碼操作來設(shè)置沒有全局額外開銷的不中斷斷點。這種方式的實現(xiàn)首先需要在內(nèi)存中的字節(jié)碼里找到我們感興趣的部分,然后在該部分的相關(guān)機(jī)器指令前插入一個函數(shù)調(diào)用。如此一來,解釋器無需任何額外的工作即可實現(xiàn)我們的不中斷斷點。

這種方法并不依靠魔法來實現(xiàn),讓我們簡要地舉個例子。

首先定義一個簡單的函數(shù):

def multiply(a, b):   result = a * b   return result

在 inspect 模塊(其包含了許多實用的單元)的文檔里,我們得知可以通過訪問 multiply.func_code.co_code 來獲取函數(shù)的字節(jié)碼:

'|\x00\x00|\x01\x00\x14}\x02\x00|\x02\x00S'

使用 Python 標(biāo)準(zhǔn)庫中的 dis 模塊可以翻譯這些不可讀的字符串。調(diào)用 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

到此,關(guān)于“怎么在Python調(diào)試過程中設(shè)置不中斷的斷點”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI