1. 程式人生 > 其它 >pdb & cProfile:除錯和效能分析的法寶

pdb & cProfile:除錯和效能分析的法寶

在實際生產環境中,對程式碼進行除錯和效能分析,是一個永遠都逃不開的話題。除錯和效能分析的主要場景,通常有這麼三個:

  • 一是程式碼本身有問題,需要我們找到 root cause 並修復;

  • 二是程式碼效率有問題,比如過度浪費資源,增加 latency,因此需要我們 debug;

  • 三是在開發新的 feature 時,一般都需要測試。

在遇到這些場景時,究竟應該使用哪些工具,如何正確的使用這些工具,應該遵循什麼樣的步驟等等,就是這節課我們要討論的話題。

用 pdb 進行程式碼除錯

pdb 的必要性

首先,我們來看程式碼的除錯。也許不少人會有疑問:程式碼除錯?說白了不就是在程式中使用 print() 語句嗎?

沒錯,在程式中相應的地方列印,的確是除錯程式的一個常用手段,但這隻適用於小型程式。因為你每次都得重新執行整個程式,或是一個完整的功能模組,才能看到打印出來的變數值。如果程式不大,每次執行都非常快,那麼使用 print(),的確是很方便的。

但是,如果我們面對的是大型程式,執行一次的除錯成本很高。特別是對於一些 tricky 的例子來說,它們通常需要反覆執行除錯、追溯上下文程式碼,才能找到錯誤根源。這種情況下,僅僅依賴列印的效率自然就很低了。 我們可以想象下面這個場景。比如你最常使用的北京圖靈學院 App,最近出現了一個 bug,部分使用者無法登陸。於是,後端工程師們開始 debug。

他們懷疑錯誤的程式碼邏輯在某幾個函式中,如果使用 print() 語句 debug,很可能出現的場景是,工程師們在他們認為的 10 個最可能出現 bug 的地方,都使用 print() 語句,然後執行整個功能塊程式碼(從啟動到執行花了 5min),看打印出來的結果值,是不是和預期相符。

如果結果值和預期相符,並能直接找到錯誤根源,顯然是最好的。但實際情況往往是,

  • 要麼與預期並不相符,需要重複以上步驟,繼續 debug;

  • 要麼雖說與預期相符,但前面的操作只是縮小了錯誤程式碼的範圍,所以仍得繼續新增 print() 語句,再一次執行相應的程式碼模組(又要 5min),進行 debug。

你可以看到,這樣的效率就很低下了。哪怕只是遇到稍微複雜一點的 case,兩、三個工程師一下午的時間可能就沒了。 可能又有人會說,現在很多的 IDE 不都有內建的 debug 工具嗎? 這話說的也沒錯。比如我們常用的 Pycharm,可以很方便地在程式中設定斷點。這樣程式只要執行到斷點處,便會自動停下,你就可以輕鬆檢視環境中各個變數的值,並且可以執行相應的語句,大大提高了除錯的效率。

看到這裡,你不禁會問,既然問題都解決了,那為什麼還要學習 pdb 呢?其實在很多大公司,產品的創造與迭代,往往需要很多程式語言的支援;並且,公司內部也會開發很多自己的介面,嘗試把儘可能多的語言給結合起來。

這就使得,很多情況下,單一語言的 IDE,對混合程式碼並不支援 UI 形式的斷點除錯功能,或是隻對某些功能模組支援。另外,考慮到不少程式碼已經挪到了類似 Jupyter 的 Notebook 中,往往就要求開發者使用命令列的形式,來對程式碼進行除錯。

而 Python 的 pdb,正是其自帶的一個除錯庫。它為 Python 程式提供了互動式的原始碼除錯功能,是命令列版本的 IDE 斷點偵錯程式,完美地解決了我們剛剛討論的這個問題。

如何使用 pdb

瞭解了 pdb 的重要性與必要性後,接下來,我們就一起來看看,pdb 在 Python 中到底應該如何使用。 首先,要啟動 pdb 除錯,我們只需要在程式中,加入“import pdb”和“pdb.set_trace()”這兩行程式碼就行了,比如下面這個簡單的例子:

a = 1
b = 2
import pdb
pdb.set_trace()
c = 3
print(a + b + c)

當我們執行這個程式時時,它的輸出介面是下面這樣的,表示程式已經執行到了“pdb.set_trace()”這行,並且暫停了下來,等待使用者輸入。

> /Users/yc/Desktop/code/demo.py(5)<module>()
-> c = 3
(Pdb) 

這時,我們就可以執行,在 IDE 斷點偵錯程式中可以執行的一切操作,比如列印,語法是”p”:

(pdb) p a
1
(pdb) p b
2

你可以看到,我列印的是 a 和 b 的值,分別為 1 和 2,與預期相符。為什麼不列印 c 呢?顯然,列印 c 會丟擲異常,因為程式目前只運行了前面幾行,此時的變數 c 還沒有被定義:

(pdb) p c
*** NameError: name 'c' is not defined

除了列印,常見的操作還有“n”,表示繼續執行程式碼到下一行,用法如下:

(pdb) n
-> print(a + b + c)

而命令”l“,則表示列舉出當前程式碼行上下的 11 行原始碼,方便開發者熟悉當前斷點周圍的程式碼狀態:

(pdb) l
  1    a = 1
  2    b = 2
  3    import pdb
  4    pdb.set_trace()
  5  ->  c = 3
  6    print(a + b + c)

命令“s“,就是 step into 的意思,即進入相對應的程式碼內部。這時,命令列中會顯示”–Call–“的字樣,當你執行完內部的程式碼塊後,命令列中則會出現”–Return–“的字樣。 我們來看下面這個例子:

def func():
    print('enter func()')
a = 1
b = 2
import pdb
pdb.set_trace()
func()
c = 3
print(a + b + c)

# pdb
> /Users/yc/test.py(9)<module>()
-> func()
(pdb) s
--Call--
> /Users/yc/test.py(1)func()
-> def func():
(Pdb) l
  1  ->  def func():
  2      print('enter func()')
  3
  4
  5    a = 1
  6    b = 2
  7    import pdb
  8    pdb.set_trace()
  9    func()
 10    c = 3
 11    print(a + b + c)
(Pdb) n
> /Users/yc/test.py(2)func()
-> print('enter func()')
(Pdb) n
enter func()
--Return--
> /Users/yc/test.py(2)func()->None
-> print('enter func()')
(Pdb) n
> /Users/yc/test.py(10)<module>()
-> c = 3

這裡,我們使用命令”s“進入了函式 func() 的內部,顯示”–Call–“;而當我們執行完函式 func() 內部語句並跳出後,顯示”–Return–“。 另外, 與之相對應的命令”r“,表示 step out,即繼續執行,直到當前的函式完成返回。 命令”b [ ([filename:]lineno | function) [, condition] ]“可以用來設定斷點。比方說,我想要在程式碼中的第 10 行,再加一個斷點,那麼在 pdb 模式下輸入”b 11“即可。 而”c“則表示一直執行程式,直到遇到下一個斷點。 當然,除了這些常用命令,還有許多其他的命令可以使用,這裡我就不在一一贅述了。你可以參考對應的官方文件(https://docs.python.org/3/library/pdb.html#module-pdb),來熟悉這些用法。

用 cProfile 進行效能分析

關於除錯的內容,我主要先講這麼多。事實上,除了要對程式進行除錯,效能分析也是每個開發者的必備技能。

日常工作中,我們常常會遇到這樣的問題:在線上,我發現產品的某個功能模組效率低下,延遲(latency)高,佔用的資源多,但卻不知道是哪裡出了問題。

這時,對程式碼進行 profile 就顯得異常重要了。

這裡所謂的 profile,是指對程式碼的每個部分進行動態的分析,比如準確計算出每個模組消耗的時間等。

這樣你就可以知道程式的瓶頸所在,從而對其進行修正或優化。當然,這並不需要你花費特別大的力氣,在 Python 中,這些需求用 cProfile 就可以實現。

舉個例子,比如我想計算斐波拉契數列,運用遞迴思想,我們很容易就能寫出下面這樣的程式碼:

def fib(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)
def fib_seq(n):
    res = []
    if n > 0:
        res.extend(fib_seq(n-1))
    res.append(fib(n))
    return res
fib_seq(30)

接下來,我想要測試一下這段程式碼總的效率以及各個部分的效率。那麼,我就只需在開頭匯入 cProfile 這個模組,並且在最後執行 cProfile.run() 就可以了:

import cProfile

def fib(n):...
def fib_seq(n):...

cProfile.run('fib_seq(30)')

或者更簡單一些,直接在執行指令碼的命令中,加入選項“-m cProfile”也很方便:

python3 -m cProfile xxx.py

執行完畢後,我們可以看到下面這個輸出介面:

         7049218 function calls (96 primitive calls) in 2.280 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    2.280    2.280 <string>:1(<module>)
     31/1    0.000    0.000    2.280    2.280 demo.py:10(fib_seq)
7049123/31    2.280    0.000    2.280    0.074 demo.py:3(fib)
        1    0.000    0.000    2.280    2.280 {built-in method builtins.exec}
       31    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
       30    0.000    0.000    0.000    0.000 {method 'extend' of 'list' objects}

這裡有一些引數你可能比較陌生,我來簡單介紹一下:

  • ncalls,是指相應程式碼 / 函式被呼叫的次數;

  • tottime,是指對應程式碼 / 函式總共執行所需要的時間(注意,並不包括它呼叫的其他程式碼 / 函式的執行時間);

  • tottime percall,就是上述兩者相除的結果,也就是tottime / ncalls;

  • cumtime,則是指對應程式碼 / 函式總共執行所需要的時間,這裡包括了它呼叫的其他程式碼 / 函式的執行時

  • cumtime percall,則是 cumtime 和 ncalls 相除的平均結果。

瞭解這些引數後,再來看這張圖。我們可以清晰地看到,這段程式執行效率的瓶頸,在於第二行的函式 fib(),它被呼叫了 700 多萬次。

有沒有什麼辦法可以提高改進呢?答案是肯定的。通過觀察,我們發現,程式中有很多對 fib() 的呼叫,其實是重複的,那我們就可以用字典來儲存計算過的結果,防止重複。

改進後的程式碼如下所示:

def memoize(f):
    memo = {}
    def helper(x):
        if x not in memo:            
            memo[x] = f(x)
        return memo[x]
    return helper
@memoize
def fib(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)
def fib_seq(n):
    res = []
    if n > 0:
        res.extend(fib_seq(n-1))
    res.append(fib(n))
    return res
fib_seq(30)

這時,我們再對其進行 profile,你就會得到新的輸出結果,很明顯,效率得到了極大的提高。

這個簡單的例子,便是 cProfile 的基本用法,也是我今天想講的重點。當然,cProfile 還有很多其他功能,還可以結合 stats 類來使用,你可以閱讀相應的官方文件來了解。

總結

這節課,我們一起學習了 Python 中常用的除錯工具 pdb,和經典的效能分析工具 cProfile。

pdb 為 Python 程式提供了一種通用的、互動式的高效率除錯方案;

而 cProfile 則是為開發者提供了每個程式碼塊執行效率的詳細分析,有助於我們對程式的優化與提高。

關於它們的更多用法,你可以通過它們的官方文件進行實踐,都不太難,熟能生巧。