如何進(jìn)行 Python性能分析矛紫,你才能如魚得水赎瞎?

【編者按】本文作者為 Bryan Helmig,主要介紹 Python 應(yīng)用性能分析的三種進(jìn)階方案颊咬。文章系國內(nèi) ITOM 管理平臺 OneAPM 編譯呈現(xiàn)务甥。

我們應(yīng)該忽略一些微小的效率提升,幾乎在 97% 的情況下喳篇,都是如此:過早的優(yōu)化是萬惡之源敞临。—— Donald Knuth

如果不先想想Knuth的這句名言麸澜,就開始進(jìn)行優(yōu)化工作挺尿,是不明智的。然而炊邦,有時你為了獲得某些特性不假思索就寫下了O(N^2) 這樣的代碼编矾,雖然你很快就忘記它們了,它們卻可能反咬你一口馁害,給你帶來麻煩:本文就是為這種情況而準(zhǔn)備的窄俏。

本文會介紹用于快速分析Python程序的一些有用工具和模式。主要目標(biāo)很簡單:盡快發(fā)現(xiàn)問題碘菜,修復(fù)問題凹蜈,并確認(rèn)修復(fù)是行之有效的限寞。

編寫一個測試

在教程開始前,要先寫一個簡單的概要測試來演示延遲踪区。你可能需要引入一些最小數(shù)據(jù)集來重現(xiàn)可觀的延遲昆烁。通常一或兩秒的運行時間,已經(jīng)足夠在發(fā)現(xiàn)問題時缎岗,讓你做出改進(jìn)了静尼。

此外,進(jìn)行一些基礎(chǔ)測試來確保你的優(yōu)化不會修改緩慢代碼的行為也是有必要的传泊。在分析和調(diào)整時鼠渺,你也可以多次運行這些測試,作為基準(zhǔn)眷细。

那么現(xiàn)在拦盹,我們來看第一個分析工具。

簡單的計時器

計時器是簡單溪椎、靈活的記錄執(zhí)行時間的方法普舆。你可以把它放到任何地方,并且?guī)缀鯖]有副作用校读。自己創(chuàng)建計時器非常簡單沼侣,并且可以根據(jù)你的喜好定制化。例如歉秫,一個簡單的計時器可以這么寫:

import time
def timefunc(f):    
  def f_timer(*args, **kwargs):
        start = time.time()
        result = f(*args, **kwargs)
        end = time.time()        
        print f.__name__, 'took', end - start, 'time'        
        return result    
     return f_timer
     
def get_number():    
  for x in xrange(5000000):        
      yield x

@timefunc
def expensive_function():    
   for x in get_number():
        i = x ^ x ^ x    
   return 'some result!'
# prints "expensive_function took 0.72583088875 seconds"
result = expensive_function()

當(dāng)然蛾洛,你可以用上下文管理器來增強它的功能,添加一些檢查點或其他小功能:

import time

class timewith():    
   def __init__(self, name=''):
        self.name = name
        self.start = time.time()    
        
   @property    
   def elapsed(self):        
     return time.time() - self.start    
     
   def checkpoint(self, name=''):        
      print '{timer} {checkpoint} took {elapsed} seconds'.format(
            timer=self.name,
            checkpoint=name,
            elapsed=self.elapsed,
        ).strip()    
        
    def __enter__(self):        
       return self    
       
    def __exit__(self, type, value, traceback):
        self.checkpoint('finished')        
        pass
        
def get_number():    
   for x in xrange(5000000):        
      yield x
      
def expensive_function():    
   for x in get_number():
        i = x ^ x ^ x    
    return 'some result!'
    
# prints something like:
# fancy thing done with something took 0.582462072372 seconds
# fancy thing done with something else took 1.75355315208 seconds
# fancy thing finished took 1.7535982132 seconds
with timewith('fancy thing') as timer:
    expensive_function()
    timer.checkpoint('done with something')
    expensive_function()
    expensive_function()
    timer.checkpoint('done with something else')
    
# or directly
timer = timewith('fancy thing')
expensive_function()
timer.checkpoint('done with something')

有了計時器雁芙,你還需要進(jìn)行一些“挖掘”工作轧膘。 封裝一些更為高級的函數(shù),然后確定問題根源之所在兔甘,進(jìn)而深入可疑的函數(shù)谎碍,不斷重復(fù)。當(dāng)你發(fā)現(xiàn)運行特別緩慢的代碼之后洞焙,修復(fù)它椿浓,然后進(jìn)行測試以確認(rèn)修復(fù)成功。

提示:不要忘了便捷的 timeit 模塊闽晦!將它用于小段代碼塊的基準(zhǔn)校驗比實際測試更加有用扳碍。

  • 計時器的優(yōu)點:容易理解和實施,也非常容易在修改前后進(jìn)行對比仙蛉,對于很多語言都適用笋敞。
  • 計時器的缺點:有時候,對于非常復(fù)雜的代碼庫而已太過簡單荠瘪,你可能會花更多的時間創(chuàng)建夯巷、替換樣板代碼赛惩,而不是修復(fù)問題!

內(nèi)建分析器

內(nèi)建分析器就好像大型槍械趁餐。雖然非常強大喷兼,但是有點不太好用,有時后雷,解釋和操作起來比較困難季惯。

你可以點此閱讀更多關(guān)于內(nèi)建分析模塊的內(nèi)容,但是內(nèi)建分析器的基本操作非常簡單:你啟用和禁用分析器臀突,它能記錄所有的函數(shù)調(diào)用和執(zhí)行時間勉抓。接著,它能為你編譯和打印輸出候学。一個簡單的分析器用例如下:

import cProfile

def do_cprofile(func):    
  def profiled_func(*args, **kwargs):
        profile = cProfile.Profile()        
        try:
            profile.enable()
            result = func(*args, **kwargs)
            profile.disable()            
            return result        
            finally:
            profile.print_stats()    
        return profiled_func
        
def get_number():    
   for x in xrange(5000000):          
      yield x
      
@do_cprofile
def expensive_function():    
   for x in get_number():
        i = x ^ x ^ x    
   return 'some result!'
   
# perform profiling
result = expensive_function()

在上面代碼中藕筋,控制臺打印的內(nèi)容如下:

         5000003 function calls in 1.626 seconds

   Ordered by: standard name   
   ncalls  tottime  percall  cumtime  percall filename:lineno(function)  
   5000001    0.571    0.000    0.571    0.000 timers.py:92(get_number)        
   1    1.055    1.055    1.626    1.626 timers.py:96(expensive_function)        
   1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

如你所見,它給出了不同函數(shù)調(diào)用的詳細(xì)數(shù)據(jù)梳码。但是隐圾,它遺漏了一項關(guān)鍵信息:是什么原因,導(dǎo)致函數(shù)運行如此緩慢掰茶?

然而翎承,這對于基礎(chǔ)分析來說是個好的開端。有時符匾,能夠幫你盡快找到解決方案。我經(jīng)常在開始調(diào)試過程時瘩例,把它作為基本測試啊胶,然后再深入測試某個不是運行緩慢,就是調(diào)用頻繁的特定函數(shù)垛贤。

  • 內(nèi)建分析器的優(yōu)點:沒有外部依賴焰坪,運行非常快聘惦。對于快速的概要測試非常有用某饰。
  • 內(nèi)建分析器的缺點:信息相對有限,需要進(jìn)一步的調(diào)試善绎;報告不太直觀黔漂,尤其是對于復(fù)雜的代碼庫。

Line Profiler

如果內(nèi)建分析器是大型槍械,line profiler就好比是離子炮。它非常的重量級且強大枕磁,使用起來也非常有趣蜕衡。

在這個例子里振坚,我們會用非常棒的kernprof line-profiler噩峦,作為 line_profiler PyPi包笑窜。為了方便使用程储,我們會再次用裝飾器進(jìn)行封裝鳍置,同時也可以防止我們把它留在生產(chǎn)代碼里(因為它比蝸牛還慢)辽剧。

try:    
   from line_profiler import LineProfiler    
   
   def do_profile(follow=[]):        
      def inner(func):            
         def profiled_func(*args, **kwargs):                
            try:
                    profiler = LineProfiler()
                    profiler.add_function(func)                    
                    for f in follow:
                        profiler.add_function(f)
                    profiler.enable_by_count()                    
                    return func(*args, **kwargs)                
             finally:
                    profiler.print_stats()            
         return profiled_func        
       return inner
       
except ImportError:    
  def do_profile(follow=[]):        
    "Helpful if you accidentally leave in production!"        
    def inner(func):            
       def nothing(*args, **kwargs):                
          return func(*args, **kwargs)            
       return nothing        
     return inner
     
def get_number():    
   for x in xrange(5000000):        
     yield x
     
     
@do_profile(follow=[get_number])
def expensive_function():    
   for x in get_number():
        i = x ^ x ^ x    
   return 'some result!'

result = expensive_function()

如果運行上面的代碼,就會看到以下的報告:

Timer unit: 1e-06 s

File: test.py
Function: get_number at line 43Total time: 4.44195 s

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================    
43                                           def get_number():    
44   5000001      2223313      0.4     50.1      for x in xrange(5000000):    
45   5000000      2218638      0.4     49.9          yield x

File: test.py
Function: expensive_function at line 47Total time: 16.828 s

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================    
47                                           def expensive_function():    
48   5000001     14090530      2.8     83.7      for x in get_number():    
49   5000000      2737480      0.5     16.3          i = x ^ x ^ x    
50         1            0      0.0      0.0      return 'some result!'

如你所見税产,這是一個非常詳細(xì)的報告怕轿,能讓你完全洞悉代碼的運行情況。和內(nèi)建的cProfiler不同砖第,它能分析核心語言特性的耗時撤卢,比如循環(huán)或?qū)耄⑶医o出不同代碼行的耗時累計值梧兼。

這些細(xì)節(jié)能讓我們更容易理解函數(shù)內(nèi)部原理放吩。 此外,如果需要研究第三方庫羽杰,你可以將其導(dǎo)入渡紫,直接輸?shù)窖b飾器中。

提示:將測試函數(shù)封裝為裝飾器考赛,再將問題函數(shù)作為參數(shù)傳進(jìn)去就好了惕澎!

  • Line Profiler 的優(yōu)點:有非常直接和詳細(xì)的報告。能夠追蹤第三方庫里的函數(shù)颜骤。
  • Line Profiler 的缺點:因為系統(tǒng)開銷巨大唧喉,會比實際執(zhí)行時間慢一個數(shù)量級,所以不要用它進(jìn)行基準(zhǔn)測試忍抽。同時八孝,它是外部工具。

結(jié)論和最佳方案

你應(yīng)該使用簡單的工具(比如計時器或內(nèi)建分析器)對測試用例(特別是那些你非常熟悉的代碼)進(jìn)行基本檢查鸠项,然后使用更慢但更加細(xì)致的工具干跛,比如 line_profiler,深入檢查函數(shù)內(nèi)部祟绊。

十有八九楼入,你會發(fā)現(xiàn)一個愚蠢的錯誤,比如在循環(huán)內(nèi)重復(fù)調(diào)用牧抽,或是使用了錯誤的數(shù)據(jù)結(jié)構(gòu)嘉熊,消耗了90%的函數(shù)執(zhí)行時間。在進(jìn)行快速(且令人滿意的)調(diào)整之后扬舒,問題就能得到解決记舆。

如果你仍然覺得程序運行太過緩慢,然后開始進(jìn)行對比屬性訪問(ttribute accessing)方法呼巴,或調(diào)整相等檢查(equality checking)方法等晦澀的調(diào)整泽腮,你可能已經(jīng)適得其反了御蒲。你應(yīng)該考慮如下方法:

1.忍受緩慢或者預(yù)先計算/緩存
2.重新思考整個實施方法
3.使用更多的優(yōu)化數(shù)據(jù)結(jié)構(gòu)(通過 Numpy,Pandas等)
4.編寫一個 C擴展

注意诊赊,優(yōu)化代碼會帶來有罪惡感的快樂厚满!尋找加速Python的合理方法很有趣,但是不要因為加速碧磅,破壞了本身的邏輯碘箍。易讀的代碼比運行速度更重要。實施緩存鲸郊,往往是最簡單的解決方法丰榴。

教程到此為止,希望你今后的Python性能分析能夠如魚得水秆撮!

PS: 點此查看代碼實例四濒。此外,點此學(xué)習(xí)如何如魚得水地調(diào)試 Python 程序职辨。

OneAPM 能幫你查看 Python 應(yīng)用程序的方方面面盗蟆,不僅能夠監(jiān)控終端的用戶體驗,還能監(jiān)控服務(wù)器性能舒裤,同時還支持追蹤數(shù)據(jù)庫喳资、第三方 API 和 Web 服務(wù)器的各種問題。想閱讀更多技術(shù)文章腾供,請訪問 OneAPM 官方技術(shù)博客仆邓。

本文轉(zhuǎn)自 OneAPM 官方博客

原文地址:https://zapier.com/engineering/profiling-python-boss/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市伴鳖,隨后出現(xiàn)的幾起案子节值,更是在濱河造成了極大的恐慌,老刑警劉巖黎侈,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異闷游,居然都是意外死亡峻汉,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進(jìn)店門脐往,熙熙樓的掌柜王于貴愁眉苦臉地迎上來休吠,“玉大人,你說我怎么就攤上這事业簿×鼋福” “怎么了?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵梅尤,是天一觀的道長柜思。 經(jīng)常有香客問我岩调,道長,這世上最難降的妖魔是什么赡盘? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任号枕,我火速辦了婚禮,結(jié)果婚禮上陨享,老公的妹妹穿的比我還像新娘葱淳。我一直安慰自己,他們只是感情好抛姑,可當(dāng)我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布赞厕。 她就那樣靜靜地躺著,像睡著了一般定硝。 火紅的嫁衣襯著肌膚如雪皿桑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天喷斋,我揣著相機與錄音唁毒,去河邊找鬼。 笑死星爪,一個胖子當(dāng)著我的面吹牛浆西,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播顽腾,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼近零,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了抄肖?” 一聲冷哼從身側(cè)響起久信,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎漓摩,沒想到半個月后裙士,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡管毙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年腿椎,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片夭咬。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡啃炸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出卓舵,到底是詐尸還是另有隱情南用,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站裹虫,受9級特大地震影響肿嘲,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜恒界,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一睦刃、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧十酣,春花似錦涩拙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至虾宇,卻和暖如春搓彻,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背嘱朽。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工旭贬, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人搪泳。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓稀轨,卻偏偏與公主長得像,于是被迫代替她去往敵國和親岸军。 傳聞我的和親對象是個殘疾皇子奋刽,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容