【編者按】本文作者為來(lái)自 HumanGeo 的工程師 Davis,主要介紹了用于 Python 應(yīng)用性能分析的幾個(gè)工具滞详。由國(guó)內(nèi) ITOM 管理平臺(tái) OneAPM 編譯呈現(xiàn)。
在 HumanGeo葫督,我們廣泛使用 Python 進(jìn)行編程唆途,并且樂(lè)趣無(wú)窮。用 Python 寫的程序不僅整潔美觀缀辩,而且運(yùn)行速度快得驚人工碾。不論是私底下還是工作中弱睦,Python 都是筆者最愛(ài)的語(yǔ)言。然而渊额,即便是 Python 這樣美妙的語(yǔ)言况木,卻也可能出現(xiàn)運(yùn)行緩慢的情況。幸運(yùn)的是旬迹,有許多不錯(cuò)的工具火惊,可以幫助我們分析 Python 代碼,從而保證其運(yùn)行效率奔垦。
當(dāng)筆者剛開始在 HumanGeo 工作時(shí)屹耐,就曾遇到過(guò)一個(gè)運(yùn)行一次耗時(shí)數(shù)小時(shí)的程序,而筆者的任務(wù)椿猎,就是找出其性能瓶頸惶岭,再盡可能地提高其運(yùn)行效率。當(dāng)時(shí)犯眠,筆者使用了許多工具按灶,包括 cProfile, PyCallGraph(源碼)筐咧,甚至 PyPy(一個(gè)運(yùn)行快速的 Python 解釋器)鸯旁,以確定最佳的程序優(yōu)化方案。在本文中量蕊,筆者將介紹上述工具(為了保持生產(chǎn)環(huán)境中的解釋器一致性铺罢,本文將不會(huì)介紹 PyPy 工具)的使用方法。甚至即便是最老練的開發(fā)者危融,也可以借助這些工具進(jìn)一步優(yōu)化他們的代碼畏铆。
免責(zé)聲明:不要過(guò)早地進(jìn)行優(yōu)化!有關(guān)過(guò)早優(yōu)化的詳細(xì)分析請(qǐng)查閱本文吉殃。
工具
閑話少敘辞居,下面開始介紹分析 Python 代碼的幾種便捷工具。
cProfile
CPython distribution 自帶兩種分析工具:profile
與 cProfile
蛋勺。兩者使用同樣的 API瓦灶,按理說(shuō)運(yùn)行效果應(yīng)該差不多。然而抱完,前者的運(yùn)行時(shí)開銷更大贼陶,因此,本文將主要介紹 cProfile
。
借助 cProfile
碉怔,可以輕松實(shí)現(xiàn)對(duì)代碼的深入分析烘贴,并且了解代碼的哪些部分亟待提升。查看下面的緩慢代碼實(shí)例:
--> % 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())
在上面的代碼中撮胧,筆者通過(guò)調(diào)用 time.sleep
方法桨踪,模擬一個(gè)運(yùn)行時(shí)間很長(zhǎng)的程序,并假定運(yùn)行結(jié)果很重要芹啥。接下來(lái)锻离,對(duì)這段代碼進(jìn)行分析,結(jié)果如下:
--> % 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}
我們發(fā)現(xiàn)墓怀,分析結(jié)果相當(dāng)瑣碎汽纠。其實(shí),可以用更有益的方式組織分析結(jié)果傀履。在上例中虱朵,調(diào)用列表是按照字母順序排列的,這對(duì)我們并無(wú)價(jià)值啤呼。筆者更愿意看到按照調(diào)用次數(shù)或累計(jì)運(yùn)行時(shí)間排列的調(diào)用情況卧秘。幸運(yùn)的是呢袱,通過(guò) -s
參數(shù)就能實(shí)現(xiàn)這一點(diǎn)官扣。我們馬上就能看到存在問(wèn)題的代碼段了!
--> % 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}
果然羞福!我們發(fā)現(xiàn)惕蹄,存在問(wèn)題的代碼就在 expensive
函數(shù)當(dāng)中。該函數(shù)在執(zhí)行結(jié)束之前調(diào)用了多次 time.sleep
方法治专,因此導(dǎo)致了程序的速度下降卖陵。
-s
參數(shù)的有效取值列表可以在此 Python 文檔中找到。如果你想將分析結(jié)果保存到一個(gè)文件中张峰,記得使用輸出選項(xiàng) -o
泪蔫。
基本功能介紹完畢之后,讓我們來(lái)看看使用分析工具查找問(wèn)題代碼的其他方法喘批。
PyCallGraph
PyCallGraph 可以看做是 cProfile
的可視化擴(kuò)展工具撩荣。借助該工具,我們可以通過(guò)出色的 Graphviz 圖片了解代碼執(zhí)行的路徑饶深。PyCallGraph 并未包含在標(biāo)準(zhǔn)的 Python 安裝包內(nèi)餐曹,因此,需要通過(guò)如下語(yǔ)句敌厘,進(jìn)行簡(jiǎn)單的安裝:
-> % pip install pycallgraph
通過(guò)下面的指令台猴,就能運(yùn)行圖形化應(yīng)用:
-> % pycallgraph graphviz -- python slow.py
運(yùn)行完畢之后,在運(yùn)行腳本的目錄下會(huì)出現(xiàn)一張 pycallgraph.png 圖片文件。同時(shí)饱狂,還應(yīng)該得到相似的分析結(jié)果(如果你之前已經(jīng)用 cProfile
分析過(guò)了)曹步。結(jié)果中的數(shù)據(jù)應(yīng)該與 cProfile
提供的結(jié)果一致。不過(guò)休讳,PyCallGraph 的優(yōu)點(diǎn)在于箭窜,它能展示被調(diào)用函數(shù)相互間的關(guān)系。
讓我們來(lái)看看圖片到底長(zhǎng)什么樣:
這多方便把苄取磺樱!圖片顯示了程序的運(yùn)行路徑,告訴我們程序經(jīng)歷過(guò)的每個(gè)函數(shù)婆咸、模塊以及文件竹捉,還帶有運(yùn)行時(shí)間與調(diào)用次數(shù)等信息。如果在龐大的應(yīng)用中運(yùn)行該分析工具尚骄,會(huì)得到一張巨大的圖片块差。但是,根據(jù)顏色的差別倔丈,我們?nèi)阅茌p易找到存在問(wèn)題的代碼塊憨闰。下面是 PyCallGraph 文檔中提供的一張圖片,展示了一段復(fù)雜的正則表達(dá)式調(diào)用中代碼的運(yùn)行路徑:
這些信息有什么用鹉动?
一旦我們確定了導(dǎo)致問(wèn)題代碼的根源,就可以選擇合適的解決方案優(yōu)化代碼泽示,為其提速蜜氨。下面械筛,讓我們根據(jù)特定的情況,探討一些緩慢代碼可行的解決方案飒炎。
I/O
如果你發(fā)現(xiàn)自己的代碼嚴(yán)重依賴于輸入/輸出,譬如赤赊,需要發(fā)送很多 Web 請(qǐng)求,那么怒竿,Python 的標(biāo)準(zhǔn)線程模塊或許就能幫你解決該問(wèn)題砍鸠。由于 CPython 的全局鎖機(jī)制(Global Interpreter Lock,GIL)不允許為代碼中心任務(wù)同時(shí)使用多個(gè)核爷辱,非 I/O 相關(guān)的線程并不適合用 Python 實(shí)現(xiàn)。
正則表達(dá)式
人們都說(shuō)饭弓,一旦你決定用正則表達(dá)式解決某個(gè)問(wèn)題弟断,你就有兩個(gè)問(wèn)題要解決了。正則表達(dá)式真的很難用對(duì)阀趴,而且難以維護(hù)。關(guān)于這一點(diǎn)棚菊,筆者可以寫一篇長(zhǎng)篇大論進(jìn)行闡述叔汁。(但是,我不會(huì)寫的:)码邻。正則表達(dá)式真的不簡(jiǎn)單另假,我相信有很多博文已經(jīng)做了詳盡的闡述。)不過(guò)开睡,在此苟耻,筆者將介紹幾個(gè)有用的技巧:
- 避免使用
.*
扶檐,貪婪的匹配所有運(yùn)算符運(yùn)行起來(lái)非常慢凶杖,盡可能使用字符類才是更好的選擇款筑。 - 避免使用正則表達(dá)式奈梳!其實(shí),許多正則表達(dá)式都可以用簡(jiǎn)單的字符串方法替代漆撞,比如
str.startswith
與str.endswith
方法。閱讀str
文檔可以找到更多有用的信息浮驳。 - 多使用
re.VERBOSE
至会!Python 的正則表達(dá)式引擎非常強(qiáng)大,超級(jí)有用宵蛀,一定要好好利用县貌!
以上是有關(guān)正則表達(dá)式筆者想說(shuō)的全部?jī)?nèi)容。如果你想要更多信息瞳别,相信網(wǎng)絡(luò)上還有很多好的文章杭攻。
Python 代碼
以筆者之前剖析過(guò)的代碼為例,我們的 Python 函數(shù)會(huì)運(yùn)行成千上萬(wàn)次以找出英文詞的詞根馆铁。該函數(shù)最迷人的地方在于锅睛,其進(jìn)行的操作很容易緩存现拒。保存函數(shù)的運(yùn)行結(jié)果之后,代碼的運(yùn)行速度提升了整整十倍勋桶。而在 Python 中創(chuàng)建緩存是輕而易舉的事情:
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
該技術(shù)名為記憶(memoization)侥猬,在具體實(shí)現(xiàn)時(shí)會(huì)執(zhí)行為裝飾器,可輕易應(yīng)用在 Python 函數(shù)中鹃锈,如下所示:
import time
@memoize
def slow(you):
time.sleep(3)
print("Hello after 3 seconds, {}!".format(you))
return 3
現(xiàn)在瞧预,如果我們多次運(yùn)行該函數(shù),運(yùn)行結(jié)果就會(huì)立即出現(xiàn):
>>> slow("Davis")
Hello after 3 seconds, Davis!
3
>>> slow("Davis")
3
>>> slow("Visitor")
Hello after 3 seconds, Visitor!
3
>>> slow("Visitor")
3
對(duì)于該項(xiàng)目來(lái)說(shuō)扔茅,這是極大的速度提升召娜。而且代碼運(yùn)行起來(lái)也沒(méi)有出現(xiàn)故障。
免責(zé)聲明:請(qǐng)確保該方法只用于 pure
函數(shù)秸讹!如果將記憶(memoization)用于帶有副作用(譬如:I/O)的函數(shù)雅倒,緩存可能無(wú)法達(dá)到預(yù)期的效果。
其他情況
如果你的代碼無(wú)法使用記憶(memoization)技巧劣欢,你的算法也不像 O(n!)
這樣瘋狂裁良,或者代碼的剖析結(jié)果也沒(méi)有引人注意的地方价脾,這可能說(shuō)明你的代碼并不存在顯著的問(wèn)題。這時(shí)候犀变,你可以嘗試一下別的運(yùn)行環(huán)境或語(yǔ)言秋柄。PyPy 就是一個(gè)好的選擇,你可能還要將算法用C語(yǔ)言擴(kuò)展方法重寫一下映琳。幸運(yùn)的是蜘拉,筆者之前的項(xiàng)目并未走到這一步有鹿,但是這仍是很好的排錯(cuò)方案葱跋。
結(jié)論
剖析代碼可以幫助你理解項(xiàng)目的執(zhí)行流程源梭、找出潛在的問(wèn)題代碼稍味,以及作為開發(fā)者該如何提升程序運(yùn)行速度模庐。Python 剖析工具不但功能強(qiáng)大,簡(jiǎn)單易用怜姿,而且足夠深入以快速找出問(wèn)題根源疼燥。雖然 Python 并不是以快速著稱的語(yǔ)言,但這并不意味著你的代碼應(yīng)該拖拖拉拉但狭。管理好自己的算法撬即,適時(shí)進(jìn)行剖析,但絕不要過(guò)早優(yōu)化息罗!
OneAPM 能夠幫你查看 Python 應(yīng)用程序的方方面面迈喉,不僅能夠監(jiān)控終端的用戶體驗(yàn)温圆,還能監(jiān)控服務(wù)器性能,同時(shí)還支持追蹤數(shù)據(jù)庫(kù)得运、第三方 API 和 Web 服務(wù)器的各種問(wèn)題锅移。想閱讀更多技術(shù)文章非剃,請(qǐng)?jiān)L問(wèn) OneAPM 官方技術(shù)博客。
本文轉(zhuǎn)自 OneAPM 官方博客
原文地址:http://blog.thehumangeo.com/2015/07/28/profiling-in-python/