超干分享!如何提高 Python 的運(yùn)行速度?

Python 已經(jīng)得到了全球程序員的喜愛,但是還是遭到一些人的詬病,原因之一就是認(rèn)為它運(yùn)行緩慢。
其實(shí)某個(gè)特定程序(無論使用何種編程語言)的運(yùn)行速度是快還是慢,在很大程度上取決于編寫該程序的開發(fā)人員自身素質(zhì),以及他們編寫優(yōu)化而高效代碼的能力。
Medium 上一位小哥就詳細(xì)講了講如何讓 Python 提速 30%,以此證明代碼跑得慢不是 Python的問題,而是代碼本身的問題。
在開始進(jìn)行任何優(yōu)化之前,我們首先需要找出代碼的哪些部分使整個(gè)程序變慢。有時(shí)程序的問題很明顯,但是如果你一時(shí)不知道問題出在哪里,那么這里有一些可能的選項(xiàng):
注意:這是我將用于演示的程序,它將進(jìn)行指數(shù)計(jì)算
# slow_program.py
from decimal import *
def exp(x):
????getcontext().prec += 2
????i, lasts, s, fact, num = 0, 0, 1, 1, 1
????while?s != lasts:
????????lasts = s
????????i += 1
????????fact *= i
????????num *= x
????????s += num / fact
????getcontext().prec -= 2
????return?+s
exp(Decimal(150))
exp(Decimal(400))
exp(Decimal(3000))最簡約的“配置文件”
首先,最簡單最偷懶的方法——Unix時(shí)間命令。
~ $ time python3.8?slow_program.py
real??0m11,058s
user 0m11,050s
sys 0m0,008s如果你只能知道整個(gè)程序的運(yùn)行時(shí)間,這樣就夠了,但通常這還遠(yuǎn)遠(yuǎn)不夠。
最詳細(xì)的分析
另外一個(gè)指令是cProfile,但是它提供的信息過于詳細(xì)了。
~ $ python3.8?-m?cProfile -s time slow_program.py
?????????1297?function?calls?(1272 primitive calls)?in?11.081 seconds
???Ordered by: internal time
???ncalls tottime percall cumtime percall filename:lineno(function)
????????3???11.079????3.693???11.079????3.693?slow_program.py:4(exp)
????????1????0.000????0.000????0.002????0.002?{built-in method _imp.create_dynamic}
??????4/1????0.000????0.000???11.081???11.081?{built-in method builtins.exec}
????????6????0.000????0.000????0.000????0.000?{built-in method __new__ of type?object at 0x9d12c0}
????????6????0.000????0.000????0.000????0.000?abc.py:132(__new__)
???????23????0.000????0.000????0.000????0.000?_weakrefset.py:36(__init__)
??????245????0.000????0.000????0.000????0.000?{built-in method builtins.getattr}
????????2????0.000????0.000????0.000????0.000?{built-in method marshal.loads}
???????10????0.000????0.000????0.000????0.000?:1233(find_spec)
??????8/4????0.000????0.000????0.000????0.000?abc.py:196(__subclasscheck__)
???????15????0.000????0.000????0.000????0.000?{built-in method posix.stat}
????????6????0.000????0.000????0.000????0.000?{built-in method builtins.__build_class__}
????????1????0.000????0.000????0.000????0.000?__init__.py:357(namedtuple)
???????48????0.000????0.000????0.000????0.000?:57(_path_join)
???????48????0.000????0.000????0.000????0.000?:59( )
????????1????0.000????0.000???11.081???11.081?slow_program.py:1( ) 在這里,我們使用cProfile模塊和time參數(shù)運(yùn)行測試腳本,以便按內(nèi)部時(shí)間(cumtime)對(duì)行進(jìn)行排序。這給了我們很多信息,你在上面看到的行大約是實(shí)際輸出的10%。由此可見,exp函數(shù)是罪魁禍?zhǔn)?,現(xiàn)在我們可以更詳細(xì)地了解時(shí)序和性能分析。
時(shí)序特定功能
現(xiàn)在我們知道了應(yīng)當(dāng)主要關(guān)注哪里,我們可能想對(duì)運(yùn)行速度緩慢的函數(shù)計(jì)時(shí),而不用測量其余的代碼。為此,我們可以使用一個(gè)簡單的裝飾器:
def?timeit_wrapper(func):
????@wraps(func)
????def?wrapper(*args, **kwargs):
????????start = time.perf_counter() # Alternatively, you can use time.process_time()
????????func_return_val = func(*args, **kwargs)
????????end?= time.perf_counter()
????????print('{0:<10}.{1:<8} : {2:<8}'.format(func.__module__, func.__name__, end?- start))
????????return?func_return_val
????return?wrapper然后可以將此裝飾器應(yīng)用于待測功能,如下所示:
@timeit_wrapper
def exp(x):
????...
print('{0:<10} {1:<8} {2:^8}'.format('module', 'function', 'time'))
exp(Decimal(150))
exp(Decimal(400))
exp(Decimal(3000))這給出我們?nèi)缦螺敵觯?/span>
~ $ python3.8?slow_program.py
module function???time??
__main__ .exp??????: 0.003267502994276583
__main__ .exp??????: 0.038535295985639095
__main__ .exp??????: 11.728486061969306需要考慮的一件事是我們實(shí)際想要測量的時(shí)間。時(shí)間包提供time.perf_counter和time.process_time兩個(gè)函數(shù)。他們的區(qū)別在于perf_counter返回的絕對(duì)值,包括你的Python程序進(jìn)程未運(yùn)行時(shí)的時(shí)間,因此它可能會(huì)受到計(jì)算機(jī)負(fù)載的影響。另一方面,process_time僅返回用戶時(shí)間(不包括系統(tǒng)時(shí)間),這僅是你的過程時(shí)間。
讓Python程序運(yùn)行得更快,這部分會(huì)很有趣!我不會(huì)展示可以解決你的性能問題的技巧和代碼,更多地是關(guān)于構(gòu)想和策略的,這些構(gòu)想和策略在使用時(shí)可能會(huì)對(duì)性能產(chǎn)生巨大影響,在某些情況下,可以將速度提高30%。
使用內(nèi)置數(shù)據(jù)類型
這一點(diǎn)很明顯。內(nèi)置數(shù)據(jù)類型非???,尤其是與我們的自定義類型(例如樹或鏈接列表)相比。這主要是因?yàn)閮?nèi)置程序是用C實(shí)現(xiàn)的,因此在使用Python進(jìn)行編碼時(shí)我們的速度實(shí)在無法與之匹敵。
使用lru_cache緩存/記憶
我已經(jīng)在上一篇博客中展示了此內(nèi)容,但我認(rèn)為值得用簡單的示例來重復(fù)它:
import?functools
import?time
# caching up to 12 different results
@functools.lru_cache(maxsize=12)
def?slow_func(x):
????time.sleep(2) # Simulate long computation
????return?x
slow_func(1) # ... waiting for 2 sec before getting result
slow_func(1) # already cached - result returned instantaneously!
slow_func(3) # ... waiting for 2 sec before getting result上面的函數(shù)使用time.sleep模擬大量計(jì)算。第一次使用參數(shù)1調(diào)用時(shí),它將等待2秒鐘,然后才返回結(jié)果。再次調(diào)用時(shí),結(jié)果已經(jīng)被緩存,因此它將跳過函數(shù)的主體并立即返回結(jié)果。有關(guān)更多實(shí)際示例,請(qǐng)參見以前的博客文章。
使用局部變量
這與在每個(gè)作用域中查找變量的速度有關(guān),因?yàn)樗恢皇鞘褂镁植孔兞窟€是全局變量。實(shí)際上,即使在函數(shù)的局部變量(最快),類級(jí)屬性(例如self.name——較慢)和全局(例如,導(dǎo)入的函數(shù))如time.time(最慢)之間,查找速度實(shí)際上也有所不同。
你可以通過使用看似不必要的分配來提高性能,如下所示:
# Example #1
class?FastClass:
????def?do_stuff(self):
????????temp = self.value # this speeds up lookup in loop
????????for?i in?range(10000):
????????????... # Do something with `temp` here
# Example #2
import?random
def?fast_function():
????r = random.random
????for?i in?range(10000):
????????print(r()) # calling `r()` here, is faster than global random.random()使用函數(shù)
這似乎違反直覺,因?yàn)檎{(diào)用函數(shù)會(huì)將更多的東西放到堆棧上,并從函數(shù)返回中產(chǎn)生開銷,但這與上一點(diǎn)有關(guān)。如果僅將整個(gè)代碼放在一個(gè)文件中而不將其放入函數(shù)中,則由于全局變量,它的運(yùn)行速度會(huì)慢得多。因此,你可以通過將整個(gè)代碼包裝在main函數(shù)中并調(diào)用一次來加速代碼,如下所示:
def?main():
????... # All your previously global code
main()不訪問屬性
可能會(huì)使你的程序變慢的另一件事是點(diǎn)運(yùn)算符(.),它在獲得對(duì)象屬性時(shí)被使用。此運(yùn)算符使用__getattribute__觸發(fā)字典查找,這會(huì)在代碼中產(chǎn)生額外的開銷。那么,我們?nèi)绾尾拍苷嬲苊猓ㄏ拗疲┦褂盟兀?/span>
# Slow:
import?re
def?slow_func():
????for?i in?range(10000):
????????re.findall(regex, line) # Slow!
# Fast:
from?re import?findall
def?fast_func():
????for?i in?range(10000):
????????findall(regex, line) # Faster!當(dāng)心字符串
使用模數(shù)(%s)或.format()進(jìn)行循環(huán)運(yùn)行時(shí),字符串操作可能會(huì)變得非常慢。我們有什么更好的選擇?根據(jù)雷蒙德·海廷格(Raymond Hettinger)最近的推特,我們唯一應(yīng)該使用的是f-string,它是最易讀,最簡潔且最快的方法。根據(jù)該推特,這是你可以使用的方法列表——最快到最慢:
f'{s}?{t}'??# Fast!
s + ' '?+ t
' '.join((s, t))
'%s %s'?% (s, t)
'{} {}'.format(s, t)
Template('$s $t').substitute(s=s, t=t) # Slow!生成器本質(zhì)上并沒有更快,因?yàn)樗鼈儽辉试S進(jìn)行延遲計(jì)算,從而節(jié)省了內(nèi)存而不是時(shí)間。但是,保存的內(nèi)存可能會(huì)導(dǎo)致你的程序?qū)嶋H運(yùn)行得更快。這是怎么做到的?如果你有一個(gè)很大的數(shù)據(jù)集,而沒有使用生成器(迭代器),那么數(shù)據(jù)可能會(huì)溢出CPU L1緩存,這將大大減慢內(nèi)存中值的查找速度。
在性能方面,非常重要的一點(diǎn)是CPU可以將正在處理的所有數(shù)據(jù)盡可能地保存在緩存中。你可以觀看Raymond Hettingers的視頻,他在其中提到了這些問題。
優(yōu)化的首要規(guī)則是不要優(yōu)化。但是,如果確實(shí)需要,那么我希望上面這些技巧可以幫助你。但是,在優(yōu)化代碼時(shí)要小心,因?yàn)樗赡茏罱K使你的代碼難以閱讀,因此難以維護(hù),這可能超過優(yōu)化的好處。
原文鏈接:
https://towardsdatascience.com/making-python-programs-blazingly-fast-c1cd79bd1b32
覺得本文對(duì)你有幫助?請(qǐng)分享給更多人
