如何對你的Python代碼進行基准測試


啥叫做基准測試(benchmark)代碼?其實主要就是測試(benchmarking)和分析(profiling)你的代碼執行多快,並且找到代碼瓶頸(bottlenecks)在哪里。 執行該動作的主要的目的就是優化(optimization)。也許因為業務需要你並且讓你的代碼執行更快一些。 當面臨這種情況時,你就需要找出你的代碼是在哪些部分慢的。

本文覆蓋如何使用不同工具進行代碼測試。

timeit

Python再帶一個模塊timeit。你可以使用它來對小代碼進行計時。timeit模塊使用針對平台的時間函數,你可以得到非常准確的一個時間結果。

timeit模塊有個命令行接口,也可以通過導入的方式。我們將從命令行中使用開始。打開終端執行如下命令:

$ python3 -m timeit -s "[ord(x) for x in 'abcdfghi']" 100000000 loops, best of 3: 0.00907 usec per loop $ python3 -m timeit -s "[chr(int(x)) for x in '123456789']" 100000000 loops, best of 3: 0.00897 usec per loop 

發生啥啦?好吧,當你執行Python命令並且傳遞-m參數時,你是在告訴解釋器去找該模塊並且作為主要程序執行。 -s告知timeit模塊執行一次代碼。然后timeit就行執行代碼三次,並且返回該三次的平均結果。該例子太過減少,它們 之間的區別可能不太明顯。

你的機器上運行的結果可能跟我的會有一些偏差。

我們再寫個簡單函數,看看是否可以從命令行測試

# simple_func.py def my_function(): try: 1 / 0 except ZeroDivisionError: pass 

整個函數的功能就是拋出一個異常然后直接忽略。很爛的例子。要通過命令使用timeit來測試,我們需要在命令空間中導入它,確保工作目錄在當前文件目錄下,然后執行:

$ python3 -m timeit "import simple_func; simple_func.my_function()" 1000000 loops, best of 3: 0.753 usec per loop 

這里我們就導入該模塊並且直接my_function()函數。注意我們使用了;來分來導入和執行代碼。現在可以開始學習如何在Python腳本代碼內部使用timeit了。

導入timeit測試

在代碼中使用timeit模塊也是相當容易的。我們還是使用之前很爛的例子:

def my_function(): try: 1 / 0 except ZeroDivisionError: pass if __name__ == "__main__": import timeit setup = "from __main__ import my_function" print(timeit.timeit("my_function()", setup=setup)) 

我們執行通過執行該腳本來驗證。我們導入了timeit模塊,然后調用timeit函數,參數的setup字符串將導入要測試函數到timeit的作用域中。現在 我們實現自己的裝飾器測試函數。

使用裝飾器

實現自己的裝飾器計時函數是一件很有趣的事情,雖然不一定有timeit精確。

import random import time def timerfunc(func): """  A timer decorator  """ def function_timer(*args, **kwargs): """  A nested function for timing other functions  """ start = time.time() value = func(*args, **kwargs) end = time.time() runtime = end - start msg = "The runtime for {func} took {time} seconds to complete" print(msg.format(func=func.__name__, time=runtime)) return value return function_timer @timerfunc def long_runner(): for x in range(5): sleep_time = random.choice(range(1, 5)) time.sleep(sleep_time) if __name__ == "__main__": long_runner() 

該例子中,我們導入了Python標准庫的random和time模塊。然后我們創建了裝飾器函數。你將注意到它接收了一個函數以及 包含了另一函數。嵌套的函數在調用實際函數時將抓取時間,然后等待函數返回並抓取結束時間。現在我們知道函數所花費的時間,我們將它 打印出來。當然該裝飾器函數也需要將實際函數執行返回的結果返回回來。

接下來的函數就是包裝了該計時裝飾器函數。你可以看到這里使用了隨機休眠,該操作只是用來模擬耗時長程序。 也許你用來執行連接數據庫(或者較大查詢),跑線程或其他事情。

每次跑這段代碼,結果都會有些不同。自己試試!

創建計時上下文管理器

一些碼農可能更喜歡使用上下文的方式來測試小段代碼。我們來構建看看:

import random import time class MyTimer: def __init__(self): self.start = time.time() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): end = time.time() runtime = end - self.start msg = 'The function took {time} seconds to complete' print(msg.format(time=runtime)) def long_runner(): for x in range(5): sleep_time = random.choice(range(1, 5)) time.sleep(sleep_time) if __name__ == "__main__": with MyTimer(): long_runner() 

本例中,我們使用__init__方法來開始我們計時,__enter__方法不做任何事情,僅僅返回它自己。__exit__方法 中抓取了結束時間,計算了總執行時間並打印。

然后我們就使用上下文管理器來測試我們執行的代碼。

cProfile

Python內建了分析器。profile和cProfile模塊。profile模塊是純Python實現,在測試過程中可能會增加很多額外的開支。所以推薦還是使用更快的cProfile。

我們不會過細的講解該模塊,我們來看看一些例子。

>>> import cProfile >>> cProfile.run("[x for x in range(1500)]") 4 function calls in 0.000 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 0.000 0.000 <string>:1(<listcomp>) 1 0.000 0.000 0.000 0.000 <string>:1(<module>) 1 0.000 0.000 0.000 0.000 {built-in method exec} 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} 

我們來了解一下內容。第一行顯示了有4個函數調用。下一行告訴結果排序方式。根據文檔,standard name指的是最右邊的列。 這里我們有好多列數據:

  • ncalls: 調用了多少次
  • tottime: 函數所花費的總時間
  • percall: ncalls / tottime
  • cumtime: 涵蓋了該函數下的所有子函數(甚至地櫃)函數所話費的時間
  • 第二個percall: cumtime / 原始釣魚那個
  • filename: fileno (function) 每個函數響應信息

你也可以像timeit模塊一樣在命令行執行cProfile。主要的區別就是需要傳遞Python腳本而不是代碼作為參數。

python3 -m cProfile test.py

嘗試執行下該命令,看看執行結果。

line_profiler

有個第三方提供的項目叫line_profiler,它用來分析每行代碼執行時間。它也有一個叫做kenprof的腳本使用line_profiler來分析Python應用和腳本。只需使用pip來安裝該包。

 pip install line_profiler

在使用line_profiler之前我們需要一些代碼來分析。不過我向先告訴你命令行中如何調用line_profiler.實際上需要執行kernprof腳本來調用line_profiler。

 kernprof -l silly_functions.py 

執行完后將會打印出:Wrote profile results to silly_functions.py.lpro。該文件不能直接查看,是一個二進制文件。 當我們執行kernprof時,其實是在__builtins__命名空間中注入LineProfiler實例。該實例稱為profile,用來作為裝飾器函數。讓我們來寫實際腳本:

import time @profile def fast_function(): print("I'm fast function!") @profile def slow_function(): time.sleep(2) print("I'm slow function!") if __name__ == "__main__": fast_function() slow_function() 

現在我們擁有使用了profile裝飾的函數。如果你直接執行該腳本將會報NameError錯誤,因為並沒有定義profile。 所以記得再實際使用時移除@profile裝飾器函數。

我們來學習如果查看分析器的結果。有兩種辦法,第一使用line_profiler模塊讀結果文件:

$ python -m line_profiler silly_functions.py.lprof

還有種是在執行kernprof時帶上-v參數:

$ kernprof -l -v silly_functions.py

無論哪種辦法你都將得到如下結果:

I'm fast function! I'm slow function! Wrote profile results to silly_functions.py.lprof Timer unit: 1e-06 s Total time: 4e-05 s File: silly_functions.py Function: fast_function at line 3 Line # Hits Time Per Hit % Time Line Contents ============================================================== 3 @profile 4 def fast_function(): 5 1 40 40.0 100.0 print("I'm fast function!") Total time: 2.00227 s File: silly_functions.py Function: slow_function at line 7 Line # Hits Time Per Hit % Time Line Contents ============================================================== 7 @profile 8 def slow_function(): 9 1 2002114 2002114.0 100.0 time.sleep(2) 10 1 157 157.0 0.0 print("I'm slow function!") 

我們可以看到結果中顯示了每行代碼的執行時間相關信息。有6列內容。我們來看看每列代表的意思。

  • Line#: 分析的代碼行數
  • Hits: 執行次數
  • Time: 該行代碼執行時間
  • Per Hit: 每次執行時間
  • % Time: 函數中所占時間比率
  • Line Contents: 執行的代碼

如果你用IPython,你可能想知道IPython的魔術方法%lprun可以分析制定函數,甚至是具體代碼。

memory_profiler

另一個用來分析的第三方包叫做memory_profiler.它用來分析每行代碼在進程中所消耗的內存。我們也使用pip來安裝它。

pip install memory_profiler

裝好后,我們也需要代碼來評測。memory_prifiler執行方式和line_profiler非常相似,它將在__builtins__注入profiler裝飾器來測試函數。

@profile
def mem_func():
    lots_of_numbers = list(range(1500))
    x = ['letters'] * (5 ** 10)
    del lots_of_numbers
    return None

if __name__ == "__main__":
    mem_func()

本例中我們創建了1500整形list。然后我們又創建了9765625(5的10次方)字符串李彪。最后我們刪除了第一個list並返回。memory_profiler沒有提供另外一個腳本來讓你執行該程序,而是直接通過-m參數來調用模塊。

$ python -m memory_profiler memo_prof.py 
Filename: memo_prof.py

Line # Mem usage Increment Line Contents ================================================ 1 13.133 MiB 0.000 MiB @profile 2 def mem_func(): 3 13.387 MiB 0.254 MiB lots_of_numbers = list(range(1500)) 4 87.824 MiB 74.438 MiB x = ['letters'] * (5 ** 10) 5 87.938 MiB 0.113 MiB del lots_of_numbers 6 87.938 MiB 0.000 MiB return None 

結果的提供的信息就非常直觀了。包含代碼行數和改行執行完后的內存使用,接下來是內存增加量,最后是執行的代碼。

memory_profiler也提供mprof用來創建完整的內存使用報表,而不是每行結果。

$ mprof run memo_prof.py
mprof: Sampling memory every 0.1s
running as a Python program...

mprof也提供來圖示來查看應用內存消耗,需要查看圖表前我們需要先安裝matplotlib

$ pip install matplotlib

然后執行:

$ mprof plot

我們得到如下圖:

Alt text

profilehooks

最后個要介紹的第三方包是profilehooks.它擁有一系列包裝器來分期函數。安裝:

pip install profilehooks

安裝完后,我們修改上次例子替換成profilehooks:

from profilehooks import profile @profile def mem_func(): lots_of_numbers = list(range(1500)) x = ['letters'] * (5 ** 10) del lots_of_numbers return None if __name__ == "__main__": mem_func() 

僅僅要做的是導入profilehooks並且包裝到要測試的函數中。執行后:

*** PROFILER RESULTS *** mem_func (profhooks.py:3) function called 1 times 2 function calls in 0.036 seconds Ordered by: cumulative time, internal time, call count ncalls tottime percall cumtime percall filename:lineno(function) 1 0.036 0.036 0.036 0.036 profhooks.py:3(mem_func) 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} 0 0.000 0.000 profile:0(profiler) 

輸出的結果和cProfile是一樣的,你可以看看我之前對這些列的描述。profilehooks提供了兩個裝飾器。我們來看看第一個timecall,它提供了函數執行時間。

from profilehooks import timecall @timecall def mem_func(): lots_of_numbers = list(range(1500)) x = ['letters'] * (5 ** 10) del lots_of_numbers return None if __name__ == "__main__": mem_func() 

執行后你將看到如下輸出:

  mem_func (profhooks2.py:3):
    0.056 seconds

整個包裝其輸出整個函數執行時間,和timeit是一樣的。

另一個profilehooks提供的裝飾器是coverage。它用來打印函數每行代碼涵蓋數。不過我看來用處到不是很大,你可以自己試試:

*** COVERAGE RESULTS *** mem_func (profhooks3.py:3) function called 1 times @coverage def mem_func(): 1: lots_of_numbers = list(range(1500)) 1: x = ['letters'] * (5 ** 10) 1: del lots_of_numbers 1: return None 

最后要告訴你的是,你也可以通過-m參數來從命令行中執行profilehooks.

python -m profilehooks mymodule.py

profilehooks是一個新包,不過我到覺得應該會很有前途。

總結

本文涵蓋了需要信息。分享了如何使用Python內建模塊,timeit和cProfile來測試和分期你的代碼,我還學會了如何自己包裝計時包裝 器和上下文管理器。然后我們還學習了一些第三方包:line_profiler, memory_profiler和profilehooks。就目前,你已經學會如何分析你的代碼,查找代碼瓶頸。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM