Python 應用剖析工具介紹

【編者按】本文做者爲來自 HumanGeo 的工程師 Davis,主要介紹了用於 Python 應用性能分析的幾個工具。由國內 ITOM 管理平臺 OneAPM 編譯呈現。html

HumanGeo,咱們普遍使用 Python 進行編程,而且樂趣無窮。用 Python 寫的程序不只整潔美觀,並且運行速度快得驚人。不管是私底下仍是工做中,Python 都是筆者最愛的語言。然而,即使是 Python 這樣美妙的語言,卻也可能出現運行緩慢的狀況。幸運的是,有許多不錯的工具,能夠幫助咱們分析 Python 代碼,從而保證其運行效率。python

當筆者剛開始在 HumanGeo 工做時,就曾遇到過一個運行一次耗時數小時的程序,而筆者的任務,就是找出其性能瓶頸,再儘量地提升其運行效率。當時,筆者使用了許多工具,包括 cProfilePyCallGraph(源碼),甚至 PyPy(一個運行快速的 Python 解釋器),以肯定最佳的程序優化方案。在本文中,筆者將介紹上述工具(爲了保持生產環境中的解釋器一致性,本文將不會介紹 PyPy 工具)的使用方法。甚至即使是最老練的開發者,也能夠藉助這些工具進一步優化他們的代碼。git

免責聲明:不要過早地進行優化!有關過早優化的詳細分析請查閱本文github

##工具 閒話少敘,下面開始介紹分析 Python 代碼的幾種便捷工具。正則表達式

###cProfile CPython distribution 自帶兩種分析工具:profilecProfile。二者使用一樣的 API,按理說運行效果應該差很少。然而,前者的運行時開銷更大,所以,本文將主要介紹 cProfile算法

藉助 cProfile,能夠輕鬆實現對代碼的深刻分析,而且瞭解代碼的哪些部分亟待提高。查看下面的緩慢代碼實例:數據庫

--> % cat slow.py
import time

def main():    
  sum = 0    
  for i in range(10):        
      sum += expensive(i // 2)    
  return sum
  
def expensive(t):    
   time.sleep(t)    
   return t
   
if __name__ == '__main__':
    print(main())

在上面的代碼中,筆者經過調用 time.sleep 方法,模擬一個運行時間很長的程序,並假定運行結果很重要。接下來,對這段代碼進行分析,結果以下:編程

--> % python -m cProfile slow.py
20
         34 function calls in 20.030 seconds

   Ordered by: standard name   
 ncalls  tottime  percall  cumtime  percall filename:lineno(function)        
 1    0.000    0.000    0.000    0.000 __future__.py:48(<module>)        
 1    0.000    0.000    0.000    0.000 __future__.py:74(_Feature)        
 7    0.000    0.000    0.000    0.000 __future__.py:75(__init__)       
 10    0.000    0.000   20.027    2.003 slow.py:11(expensive)        
 1    0.002    0.002   20.030   20.030 slow.py:2(<module>)        
 1    0.000    0.000   20.027   20.027 slow.py:5(main)        
 1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}        
 1    0.000    0.000    0.000    0.000 {print}        
 1    0.000    0.000    0.000    0.000 {range}       
 10   20.027    2.003   20.027    2.003 {time.sleep}

咱們發現,分析結果至關瑣碎。其實,能夠用更有益的方式組織分析結果。在上例中,調用列表是按照字母順序排列的,這對咱們並沒有價值。筆者更願意看到按照調用次數或累計運行時間排列的調用狀況。幸運的是,經過 -s 參數就能實現這一點。咱們立刻就能看到存在問題的代碼段了!緩存

--> % python -m cProfile -s calls slow.py
20
         34 function calls in 20.028 seconds

   Ordered by: call count   
   
   ncalls  tottime  percall  cumtime  percall filename:lineno(function)       
   10    0.000    0.000   20.025    2.003 slow.py:11(expensive)       
   10   20.025    2.003   20.025    2.003 {time.sleep}        
   7    0.000    0.000    0.000    0.000 __future__.py:75(__init__)        
   1    0.000    0.000   20.026   20.026 slow.py:5(main)        
   1    0.000    0.000    0.000    0.000 __future__.py:74(_Feature)        
   1    0.000    0.000    0.000    0.000 {print}        
   1    0.000    0.000    0.000    0.000 __future__.py:48(<module>)        
   1    0.003    0.003   20.028   20.028 slow.py:2(<module>)        
   1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}        
   1    0.000    0.000    0.000    0.000 {range}

果真!咱們發現,存在問題的代碼就在 expensive 函數當中。該函數在執行結束以前調用了屢次 time.sleep 方法,所以致使了程序的速度降低。服務器

-s參數的有效取值列表能夠在此 Python 文檔中找到。若是你想將分析結果保存到一個文件中,記得使用輸出選項 -o

基本功能介紹完畢以後,讓咱們來看看使用分析工具查找問題代碼的其餘方法。

###PyCallGraph PyCallGraph 能夠看作是 cProfile 的可視化擴展工具。藉助該工具,咱們能夠經過出色的 Graphviz 圖片瞭解代碼執行的路徑。PyCallGraph 並未包含在標準的 Python 安裝包內,所以,須要經過以下語句,進行簡單的安裝:

-> % pip install pycallgraph

經過下面的指令,就能運行圖形化應用:

-> % pycallgraph graphviz -- python slow.py

運行完畢以後,在運行腳本的目錄下會出現一張 pycallgraph.png 圖片文件。同時,還應該獲得類似的分析結果(若是你以前已經用 cProfile 分析過了)。結果中的數據應該與 cProfile 提供的結果一致。不過,PyCallGraph 的優勢在於,它能展現被調用函數相互間的關係。

讓咱們來看看圖片到底長什麼樣:

Python 應用剖析工具介紹

這多方便啊!圖片顯示了程序的運行路徑,告訴咱們程序經歷過的每一個函數、模塊以及文件,還帶有運行時間與調用次數等信息。若是在龐大的應用中運行該分析工具,會獲得一張巨大的圖片。可是,根據顏色的差異,咱們仍能輕易找到存在問題的代碼塊。下面是 PyCallGraph 文檔中提供的一張圖片,展現了一段複雜的正則表達式調用中代碼的運行路徑:

Python 應用剖析工具介紹

點此獲取此圖分析的源碼

##這些信息有什麼用? 一旦咱們肯定了致使問題代碼的根源,就能夠選擇合適的解決方案優化代碼,爲其提速。下面,讓咱們根據特定的狀況,探討一些緩慢代碼可行的解決方案。

###I/O 若是你發現本身的代碼嚴重依賴於輸入/輸出,譬如,須要發送不少 Web 請求,那麼,Python 的標準線程模塊或許就能幫你解決該問題。因爲 CPython 的全局鎖機制(Global Interpreter Lock,GIL)不容許爲代碼中心任務同時使用多個核,非 I/O 相關的線程並不適合用 Python 實現。

###正則表達式 人們都說,一旦你決定用正則表達式解決某個問題,你就有兩個問題要解決了。正則表達式真的很難用對,並且難以維護。關於這一點,筆者能夠寫一篇長篇大論進行闡述。(可是,我不會寫的:)。正則表達式真的不簡單,我相信有不少博文已經作了詳盡的闡述。)不過,在此,筆者將介紹幾個有用的技巧:

  1. 避免使用 .*,貪婪的匹配全部運算符運行起來很是慢,儘量使用字符類纔是更好的選擇。
  2. 避免使用正則表達式!其實,許多正則表達式均可以用簡單的字符串方法替代,好比 str.startswithstr.endswith 方法。閱讀 str 文檔能夠找到更多有用的信息。
  3. 多使用 re.VERBOSE!Python 的正則表達式引擎很是強大,超級有用,必定要好好利用!

以上是有關正則表達式筆者想說的所有內容。若是你想要更多信息,相信網絡上還有不少好的文章。

###Python 代碼 以筆者以前剖析過的代碼爲例,咱們的 Python 函數會運行成千上萬次以找出英文詞的詞根。該函數最迷人的地方在於,其進行的操做很容易緩存。保存函數的運行結果以後,代碼的運行速度提高了整整十倍。而在 Python 中建立緩存是垂手可得的事情:

from functools import wraps
def memoize(f):
    cache = {}    
    @wraps(f)    
    def inner(arg):       
       if arg not in cache:
            cache[arg] = f(arg)        
       return cache[arg]   
     return inner

該技術名爲記憶(memoization),在具體實現時會執行爲裝飾器,可輕易應用在 Python 函數中,以下所示:

import time
@memoize
def slow(you):
    time.sleep(3)
    print("Hello after 3 seconds, {}!".format(you))    
    return 3

如今,若是咱們屢次運行該函數,運行結果就會當即出現:

>>> slow("Davis")
Hello after 3 seconds, Davis!
3
>>> slow("Davis")
3
>>> slow("Visitor")
Hello after 3 seconds, Visitor!
3
>>> slow("Visitor")
3

對於該項目來講,這是極大的速度提高。並且代碼運行起來也沒有出現故障。

免責聲明:請確保該方法只用於 pure 函數!若是將記憶(memoization)用於帶有反作用(譬如:I/O)的函數,緩存可能沒法達到預期的效果。

###其餘狀況 若是你的代碼沒法使用記憶(memoization)技巧,你的算法也不像 O(n!) 這樣瘋狂,或者代碼的剖析結果也沒有引人注意的地方,這可能說明你的代碼並不存在顯著的問題。這時候,你能夠嘗試一下別的運行環境或語言。PyPy 就是一個好的選擇,你可能還要將算法用C語言擴展方法重寫一下。幸運的是,筆者以前的項目並未走到這一步,可是這還是很好的排錯方案。

##結論 剖析代碼能夠幫助你理解項目的執行流程、找出潛在的問題代碼,以及做爲開發者該如何提高程序運行速度。Python 剖析工具不但功能強大,簡單易用,並且足夠深刻以快速找出問題根源。雖然 Python 並非以快速著稱的語言,但這並不意味着你的代碼應該拖拖拉拉。管理好本身的算法,適時進行剖析,但毫不要過早優化!

OneAPM 可以幫你查看 Python 應用程序的方方面面,不只可以監控終端的用戶體驗,還能監控服務器性能,同時還支持追蹤數據庫、第三方 API 和 Web 服務器的各類問題。想閱讀更多技術文章,請訪問 OneAPM 官方技術博客

本文轉自 OneAPM 官方博客

原文地址:http://blog.thehumangeo.com/2015/07/28/profiling-in-python/

相關文章
相關標籤/搜索