讓Python程序快30%的技巧
一直以來Python性能是遭人詬病的問題之一,抱怨執(zhí)行慢,沒法用。雖然再性能上語言的差異確實存在著明顯差異,但是我認(rèn)為一個非常流行的語言,運行的快慢不會成為阻擾人們使用的因素。如果是的話,可能是由于編寫的程序有問題,需要優(yōu)化。本文蟲蟲就給大家介紹一下如何調(diào)試Python應(yīng)用的性能,以及怎么對其進行優(yōu)化。
Python性能調(diào)試
要進行Python性能,前提條件是要找出程序中的性能瓶頸。找出程序中影響程序性能的代碼。有經(jīng)驗的開發(fā)者一般都能很容易能找出程序的瓶頸,但對于普通碼農(nóng)找出系統(tǒng)的問題代碼則很難,為了能快捷有效的發(fā)現(xiàn)程序的性能瓶頸就需要進行性能調(diào)試,此處我們以一個實際例子進行介紹,以下程序是計算e的x(1..n)次的冪,其代碼如下:
- # performance.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
- print(exp(Decimal(150)))
- print(exp(Decimal(400)))
- print(exp(Decimal(3000)))
最簡單的調(diào)試
最簡單且實用的調(diào)試性能調(diào)試的方法是使用Linux的time命令,time可以計算程序執(zhí)行的時間:

- time python3 performance.py
- 1.393709580666379697318341937E+65
- 5.221469689764143950588763007E+173
- 7.646200989054704889310727660E+1302
- real 0m15.185s
- user 0m15.100s
- sys 0m0.004s
計算前兩個數(shù)的(150,400)很快,而第三個大一點時會很慢,總共要15秒多才算完,是有點卡頓(慢)。
time雖然很便捷有用,但是不能給我們詳細(xì)的代碼性能細(xì)節(jié)。
詳細(xì)性能分析cProfile
性能分析另一個常用的方法是使用cProfile,它可以提供很多性能信息
- python3 -m cProfile -s time performance.py

例子中,我們使用了cProfile模塊和time參數(shù)運行測試腳本,以便按內(nèi)部時間(cumtime)對行進行排序。如上圖所示,使用cProfile可以給很多內(nèi)部的具體信息,通過我們可以知道主要耗時是由exp函數(shù)導(dǎo)致。知道了程序的性能瓶頸所在,我們就再說明Python性能分析和優(yōu)化。
優(yōu)化特定功能
知道了將性能的瓶頸所在(實例中是exp函數(shù)),我們?yōu)榱诉M一步具體問題具體分析,我們使用一個簡單裝飾器,以便跳過其他代碼,專門分析性能瓶頸所設(shè)計的函數(shù)。然后使用裝飾器進行測試,具體代碼如下:
- 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
我們用這個裝飾器來測試exp:
- @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))

結(jié)果:
- module function time
- __main__ .exp : 0.00920036411844194
- __main__ .exp : 0.09822067408822477
- __main__ .exp : 15.228459489066154
代碼中,我們用到了time包提供time.perf_counter函數(shù),它還提供了另外一個函數(shù)time.process_time。兩者的區(qū)別在于perf_counter返回的絕對時間,包括Python程序進程未運行時的時間,它可能會受到計算機負(fù)載的影響。而process_time僅返回用戶時間(不包括系統(tǒng)時間),這僅是程序過程時間。
性能優(yōu)化
最后是Python程序的性能優(yōu)化,為了讓Python程序運行得更快,我們提供一些可供參考的性能優(yōu)化構(gòu)想和策略的,通過這些策略我們一半可以提高應(yīng)用的運行速度,最高情況下可以讓你的應(yīng)用快30%。
使用內(nèi)建數(shù)據(jù)類型
很明顯,內(nèi)建數(shù)據(jù)類型非常快,尤其是與自定義類型相比,比如樹或者鏈表。因為內(nèi)建程序是用C實現(xiàn)的,所以其性能優(yōu)勢是Python代碼所無法比擬的。
使用lru_cache緩存/記憶
很多時候緩存非常有效,可以極大的提高性能,尤其在數(shù)值計算和涉及大量重復(fù)調(diào)用(遞歸)時??紤]一個例子:

上面的函數(shù)使用time.sleep(2)模擬一個耗時的代碼。第一次使用參數(shù)1調(diào)用時,它將等待2秒,然后返回結(jié)果。再次調(diào)用時,由于結(jié)果已被緩存,將跳過函數(shù)的執(zhí)行,直返回。用3調(diào)用時候由于參數(shù)不一樣會耗時2秒,總體耗時應(yīng)該為4s,我們用time 驗證:
- real 0m4.061s
- user 0m0.040s
- sys 0m0.015s
這和我們設(shè)想的一致。
使用局部變量
基于變量作用域中查找速度相關(guān),在函數(shù)的局部變量具有很高的速度。其次是類級屬性(如self.name)和最慢的是全局變量,如time.time(最慢)。所以我們可以通過避免使用不必要的全局變量來提高性能。
使用函數(shù)
這似乎有點出乎意料,因為涉及函數(shù)的內(nèi)存占用都在堆棧上,而函數(shù)返回也會有開銷。但是使用函數(shù),可以避免使用全局變量,可以提高性能。因此,可以通過將整個代碼包裝在main函數(shù)中只調(diào)用一次來加速代碼。
避免使用屬性
另一個可以是影響程序性能的操作是點運算符訪問對象屬性。點運算符使用__getattribute__觸發(fā)會字典查找,會在代碼中產(chǎn)生額外的開銷。我們可以通過一些使用函數(shù)而不是類方法的方式避免點操作,比如下面例子
#慢代碼:
- import re
- def slow_func():
- for i in range(10000):
- re.findall(regex, line)
#快代碼
- from re import findall
- def fast_func():
- for i in range(10000):
- findall(regex, line)
使用f-string
在循環(huán)中使用格式符(%s)或.format()時,字符串操作可能會變得非常緩慢。為了進行性能優(yōu)化,我們應(yīng)該使用f-string。它是Python 3.6引入的很具可讀性,簡潔性和最快的方法。比如:
- s + ' ' + t
- ' '.join((s, t))
- '%s %s' % (s, t)
- '{} {}'.format(s, t)
- Template('$s $t').substitute(s=s, t=t) # 慢代碼
- f'{s} {t}' # 快代碼
總結(jié)
性能的調(diào)試和優(yōu)化是非常重要的碼農(nóng)技術(shù)之一。本文中,我們提供了Python應(yīng)用性能調(diào)試和優(yōu)化的技巧和策略,希望能對大家有所幫助。