使用優(yōu)化器來(lái)提升Python程序的執(zhí)行效率的教程
如果不首先想想這句Knuth的名言,就開始進(jìn)行優(yōu)化工作是不明智的。可是,你很快寫出來(lái)加入一些特性的代碼,可能會(huì)很丑陋,你需要注意了。這篇文章就是為這時(shí)候準(zhǔn)備的。
那么接下來(lái)就是一些很有用的工具和模式來(lái)快速優(yōu)化Python。它的主要目的很簡(jiǎn)單:盡快發(fā)現(xiàn)瓶頸,修復(fù)它們并且確認(rèn)你修復(fù)了它們。
寫一個(gè)測(cè)試
在你開始優(yōu)化前,寫一個(gè)高級(jí)測(cè)試來(lái)證明原來(lái)代碼很慢。你可能需要采用一些最小值數(shù)據(jù)集來(lái)復(fù)現(xiàn)它足夠慢。通常一兩個(gè)顯示運(yùn)行時(shí)秒的程序就足夠處理一些改進(jìn)的地方了。
有一些基礎(chǔ)測(cè)試來(lái)保證你的優(yōu)化沒(méi)有改變?cè)写a的行為也是很必要的。你也能夠在很多次運(yùn)行測(cè)試來(lái)優(yōu)化代碼的時(shí)候稍微修改這些測(cè)試的基準(zhǔn)。
那么現(xiàn)在,我們來(lái)來(lái)看看優(yōu)化工具把。
簡(jiǎn)單的計(jì)時(shí)器
計(jì)時(shí)器很簡(jiǎn)單,這是一個(gè)最靈活的記錄執(zhí)行時(shí)間的方法。你可以把它放到任何地方并且副作用很小。運(yùn)行你自己的計(jì)時(shí)器非常簡(jiǎn)單,并且你可以將其定制,使它以你期望的方式工作。例如,你個(gè)簡(jiǎn)單的計(jì)時(shí)器如下:
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)然,你可以用上下文管理來(lái)讓它功能更加強(qiáng)大,添加一些檢查點(diǎn)或者一些其他的功能:
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ì)時(shí)器還需要你做一些挖掘。包裝一些更高級(jí)的函數(shù),并且確定瓶頸在哪,然后深入的函數(shù)里,能夠不停的重現(xiàn)。當(dāng)你發(fā)現(xiàn)一些不合適的代碼,修復(fù)它,然后測(cè)試一遍以確認(rèn)它被修復(fù)了。
一些小技巧:不要忘了好用的timeit模塊!它對(duì)小塊代碼做基準(zhǔn)測(cè)試而不是實(shí)際調(diào)查更加有用。
- Timer 優(yōu)點(diǎn):很容易理解和實(shí)現(xiàn)。也非常容易在修改后進(jìn)行比較。對(duì)于很多語(yǔ)言都適用。
- Timer 缺點(diǎn):有時(shí)候?qū)τ诜浅?fù)雜的代碼有點(diǎn)過(guò)于簡(jiǎn)單,你可能會(huì)花更多時(shí)間放置或移動(dòng)引用代碼而不是修復(fù)問(wèn)題!
內(nèi)建優(yōu)化器
啟用內(nèi)建的優(yōu)化器就像是用一門大炮。它非常強(qiáng)大,但是有點(diǎn)不太好用,使用和解釋起來(lái)比較復(fù)雜。
你可以了解更多關(guān)于profile模塊的東西,但是它的基礎(chǔ)是非常簡(jiǎn)單的:你能夠啟用和禁用優(yōu)化器,而且它能打印所有的函數(shù)調(diào)用和執(zhí)行時(shí)間。它能給你編譯和打印出輸出。一個(gè)簡(jiǎn)單的裝飾器如下:
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()
在上面代碼的情況下,你應(yīng)該看到有些東西在終端打印出來(lái),打印的內(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)用次數(shù),但它遺漏了一些關(guān)鍵的信息:是哪個(gè)函數(shù)讓運(yùn)行這么慢?
可是,這對(duì)于基礎(chǔ)優(yōu)化來(lái)說(shuō)是個(gè)好的開始。有時(shí)候甚至能用更少的精力找到解決方案。我經(jīng)常用它來(lái)在深入挖掘究竟是哪個(gè)函數(shù)慢或者調(diào)用次數(shù)過(guò)多之前來(lái)調(diào)試程序。
- 內(nèi)建優(yōu)點(diǎn):沒(méi)有額外的依賴并且非???。對(duì)于快速的高等級(jí)檢查非常有用。
- 內(nèi)建缺點(diǎn):信息相對(duì)有限,需要進(jìn)一步的調(diào)試;報(bào)告有點(diǎn)不太直接,尤其是對(duì)于復(fù)雜的代碼。
Line Profiler
如果內(nèi)建的優(yōu)化器是一門大炮,那么line profiler可以看作是一門離子加農(nóng)炮。它非常的重量級(jí)和強(qiáng)大。
在這個(gè)例子里,我們會(huì)用非常棒的line_profiler庫(kù)。為了容易使用,我們會(huì)再次用裝飾器包裝一下,這種簡(jiǎ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()
如果你運(yùn)行上面的代碼,你就可以看到一下的報(bào)告:
Timer unit: 1e-06 s File: test.py Function: get_number at line 43 Total 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 47 Total 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!'
你可以看到,有一個(gè)非常詳細(xì)的報(bào)告,能讓你完全洞悉代碼運(yùn)行的情況。不想內(nèi)建的cProfiler,它能計(jì)算話在語(yǔ)言核心特性的時(shí)間,比如循環(huán)和導(dǎo)入并且給出在不同的行花費(fèi)的時(shí)間。
這些細(xì)節(jié)能讓我們更容易理解函數(shù)內(nèi)部。如果你在研究某個(gè)第三方庫(kù),你可以直接將其導(dǎo)入并加上裝飾器來(lái)分析它。
一些小技巧:只裝飾你的測(cè)試函數(shù)并將問(wèn)題函數(shù)作為接下來(lái)的參數(shù)。
- Line Profiler 優(yōu)點(diǎn):有非常直接和詳細(xì)的報(bào)告。能夠追蹤第三方庫(kù)里的函數(shù)。
- Line Profiler 缺點(diǎn):因?yàn)樗鼤?huì)讓代碼比真正運(yùn)行時(shí)慢很多,所以不要用它來(lái)做基準(zhǔn)測(cè)試。這是額外的需求。
總結(jié)和最佳實(shí)踐
你應(yīng)該用更簡(jiǎn)單的工具來(lái)對(duì)測(cè)試用例進(jìn)行根本的檢查,并且用更慢但能顯示更多細(xì)節(jié)的line_profiler來(lái)深入到函數(shù)內(nèi)部。
九成情況下,你可能會(huì)發(fā)現(xiàn)在一個(gè)函數(shù)里循環(huán)調(diào)用或一個(gè)錯(cuò)誤的數(shù)據(jù)結(jié)構(gòu)消耗了90%的時(shí)間。一些調(diào)整工具是非常適合你的。
如果你仍然覺得這太慢,而是用一些你自己的秘密武器,如比較屬性訪問(wèn)技術(shù)或調(diào)整平衡檢查技術(shù)。你也可以用如下的方法:
1.忍受緩慢或者緩存它們
2.重新思考整個(gè)實(shí)現(xiàn)
3.更多使用優(yōu)化的數(shù)據(jù)結(jié)構(gòu)
4.寫一個(gè)C擴(kuò)展
注意了,優(yōu)化代碼是種罪惡的快感!用合適的方法來(lái)為你的Python代碼加速很有意思,但是注意不要破壞了本身的邏輯??勺x的代碼比運(yùn)行速度更重要。先把它緩存起來(lái)再進(jìn)行優(yōu)化其實(shí)更好。
相關(guān)文章
python反反爬蟲技術(shù)限制連續(xù)請(qǐng)求時(shí)間處理
這篇文章主要為大家介紹了python反反爬蟲技術(shù)限制連續(xù)請(qǐng)求時(shí)間處理,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
python GUI庫(kù)圖形界面開發(fā)之PyQt5開發(fā)環(huán)境配置與基礎(chǔ)使用
這篇文章主要介紹了python GUI庫(kù)圖形界面開發(fā)之PyQt5開發(fā)環(huán)境配置與基礎(chǔ)使用,需要的朋友可以參考下2020-02-02
利用Python操作MongoDB數(shù)據(jù)庫(kù)的詳細(xì)指南
MongoDB是由C++語(yǔ)言編寫的非關(guān)系型數(shù)據(jù)庫(kù),是一個(gè)基于分布式文件存儲(chǔ)的開源數(shù)據(jù)庫(kù)系統(tǒng),其內(nèi)容存儲(chǔ)形式類似JSON對(duì)象,下面這篇文章主要給大家介紹了關(guān)于利用Python操作MongoDB數(shù)據(jù)庫(kù)的相關(guān)資料,需要的朋友可以參考下2023-02-02
python 通過(guò)類中一個(gè)方法獲取另一個(gè)方法變量的實(shí)例
今天小編就為大家分享一篇python 通過(guò)類中一個(gè)方法獲取另一個(gè)方法變量的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-01-01
Python基于多線程實(shí)現(xiàn)抓取數(shù)據(jù)存入數(shù)據(jù)庫(kù)的方法
這篇文章主要介紹了Python基于多線程實(shí)現(xiàn)抓取數(shù)據(jù)存入數(shù)據(jù)庫(kù)的方法,結(jié)合實(shí)例形式分析了Python使用數(shù)據(jù)庫(kù)類與多線程類進(jìn)行數(shù)據(jù)抓取與寫入數(shù)據(jù)庫(kù)操作的具體使用技巧,需要的朋友可以參考下2018-06-06
Python數(shù)據(jù)處理之savetxt()和loadtxt()使用詳解
這篇文章主要介紹了Python數(shù)據(jù)處理之savetxt()和loadtxt()使用詳解,NumPy提供了多種存取數(shù)組內(nèi)容的文件操作函數(shù),保存數(shù)組數(shù)據(jù)的文件可以是二進(jìn)制格式或者文本格式,今天我們來(lái)看看savetxt()和loadtxt()的用法,需要的朋友可以參考下2023-08-08
對(duì)python中的os.getpid()和os.fork()函數(shù)詳解
今天小編就為大家分享一篇對(duì)python中的os.getpid()和os.fork()函數(shù)詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-08-08
python3+PyQt5實(shí)現(xiàn)自定義分?jǐn)?shù)滑塊部件
這篇文章主要為大家詳細(xì)介紹了python3+PyQt5實(shí)現(xiàn)自定義分?jǐn)?shù)滑塊部件,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-04-04

