歡迎光臨
每天分享高質量文章

做到這二十條,Python程式性能輕鬆翻倍!

1.優化演算法時間複雜度

演算法的時間複雜度對程式的執行效率影響最大,在Python中可以通過選擇合適的資料結構來優化時間複雜度,如list和set查找某一個元素的時間複雜度分別是O(n)和O(1)。不同的場景有不同的優化方式,總得來說,一般有分治,分支界限,貪心,動態規劃等思想。

2. 減少冗餘資料

如用上三角或下三角的方式去儲存一個大的對稱矩陣。在0元素占大多數的矩陣里使用稀疏矩陣表示。

3. 合理使用copy與deepcopy

對於dict和list等資料結構的物件,直接賦值使用的是取用的方式。而有些情況下需要複製整個物件,這時可以使用copy包里的copy和deepcopy,這兩個函式的不同之處在於後者是遞迴複製的。效率也不一樣:(以下程式在ipython中運行)

import copy
a=range(100000)
%timeit-n10copy.copy(a)# 運行10次 copy.copy(a)
%timeit-n10copy.deepcopy(a)
10loops,best of3:1.55ms per loop
10loops,best of3:151ms per loop

timeit後面的-n表示運行的次數,後兩行對應的是兩個timeit的輸出,下同。由此可見後者慢一個數量級。

4. 使用dict或set查找元素

python dict和set都是使用hash表來實現(類似c++11標準庫中unordered_map),查找元素的時間複雜度是O(1)

a=range(1000)
s=set(a)
d=dict((i,1)foriina)
%timeit-n10000100ind
%timeit-n10000100ins
10000loops,best of3:43.5ns per loop
10000loops,best of3:49.6ns per loop

dict的效率略高(占用的空間也多一些)。

5. 合理使用生成器(generator)和yield

%timeit-n100a=(iforiinrange(100000))
%timeit-n100b=[iforiinrange(100000)]
100loops,best of3:1.54ms per loop
100loops,best of3:4.56ms per loop

使用()得到的是一個generator物件,所需要的記憶體空間與串列的大小無關,所以效率會高一些。在具體應用上,比如set(i for i in range(100000))會比set([i for i in range(100000)])快。但是對於需要迴圈遍歷的情況:

%timeit-n10forxin(iforiinrange(100000)):pass
%timeit-n10forxin[iforiinrange(100000)]:pass
10loops,best of3:6.51ms per loop
10loops,best of3:5.54ms per loop

後者的效率反而更高,但是如果迴圈裡有break,用generator的好處是顯而易見的。yield也是用於創建generator:

def yield_func(ls):
    foriinls:
        yieldi+1
def not_yield_func(ls):
    return[i+1foriinls]
ls=range(1000000)
%timeit-n10foriinyield_func(ls):pass
%timeit-n10foriinnot_yield_func(ls):pass
10loops,best of3:63.8ms per loop
10loops,best of3:62.9ms per loop

對於記憶體不是非常大的list,可以直接傳回一個list,但是可讀性yield更佳(人個喜好)。python2.x內置generator功能的有xrange函式、itertools包等。

6. 優化迴圈

迴圈之外能做的事不要放在迴圈內,比如下麵的優化可以快一倍:

a=range(10000)
size_a=len(a)
%timeit-n1000foriina:k=len(a)
%timeit-n1000foriina:k=size_a
1000loops,best of3:569µsper loop
1000loops,best of3:256µsper loop

7. 優化包含多個判斷運算式的順序

對於and,應該把滿足條件少的放在前面,對於or,把滿足條件多的放在前面。如:

a=range(2000)  
%timeit-n100[iforiinaif1020or10002000]
%timeit-n100[iforiinaif10002000or10020]    
%timeit-n100[iforiinaifi%2==0andi>1900]
%timeit-n100[iforiinaifi>1900andi%2==0]
100loops,best of3:287µsper loop
100loops,best of3:214µsper loop
100loops,best of3:128µsper loop
100loops,best of3:56.1µsper loop

8. 使用join合併迭代器中的字串

In[1]:%%timeit
   ...:s=''
   ...:foriina:
   ...:         s+=i
   ...:
10000loops,best of3:59.8µsper loop
In[2]:%%timeit
s=''.join(a)
   ...:
100000loops,best of3:11.8µsper loop

join對於累加的方式,有大約5倍的提升。

9. 選擇合適的格式化字符方式

s1,s2='ax','bx'
%timeit-n100000'abc%s%s'%(s1,s2)
%timeit-n100000'abc{0}{1}'.format(s1,s2)
%timeit-n100000'abc'+s1+s2
100000loops,best of3:183ns per loop
100000loops,best of3:169ns per loop
100000loops,best of3:103ns per loop

三種情況中,%的方式是最慢的,但是三者的差距並不大(都非常快)。(個人覺得%的可讀性最好)

10. 不借助中間變數交換兩個變數的值

In[3]:%%timeit-n10000
    a,b=1,2
   ....:c=a;a=b;b=c;
   ....:
10000loops,best of3:172ns per loop
In[4]:%%timeit-n10000
a,b=1,2
a,b=b,a
   ....:
10000loops,best of3:86ns per loop

使用a,b=b,a而不是c=a;a=b;b=c;來交換a,b的值,可以快1倍以上。

11. 使用if is

a=range(10000)
%timeit-n100[iforiinaifi==True]
%timeit-n100[iforiinaifiisTrue]
100loops,best of3:531µsper loop
100loops,best of3:362µsper loop

使用 if is True 比 if == True 將近快一倍。

12. 使用級聯比較x < y < z

x,y,z=1,2,3
%timeit-n1000000ifxpass
%timeit-n1000000ifxpass
1000000loops,best of3:101ns per loop
1000000loops,best of3:121ns per loop

x < y < z效率略高,而且可讀性更好。

13. while 1 比 while True 更快

def while_1():
    n=100000
    while1:
        n-=1
        ifn<=0:break
def while_true():
    n=100000
    whileTrue:
        n-=1
        ifn<=0:break    
m,n=1000000,1000000
%timeit-n100while_1()
%timeit-n100while_true()
100loops,best of3:3.69ms per loop
100loops,best of3:5.61ms per loop

while 1 比 while true快很多,原因是在python2.x中,True是一個全域性變數,而非關鍵字。

14. 使用**而不是pow

%timeit-n10000c=pow(2,20)
%timeit-n10000c=2**20
10000loops,best of3:284ns per loop
10000loops,best of3:16.9ns per loop

**就是快10倍以上!

15. 使用 cProfile, cStringIO 和 cPickle等用c實現相同功能(分別對應profile, StringIO, pickle)的包

import cPickle
import pickle
a=range(10000)
%timeit-n100x=cPickle.dumps(a)
%timeit-n100x=pickle.dumps(a)
100loops,best of3:1.58ms per loop
100loops,best of3:17ms per loop

由c實現的包,速度快10倍以上!

16. 使用最佳的反序列化方式

下麵比較了eval, cPickle, json方式三種對相應字串反序列化的效率:

import json
import cPickle
a=range(10000)
s1=str(a)
s2=cPickle.dumps(a)
s3=json.dumps(a)
%timeit-n100x=eval(s1)
%timeit-n100x=cPickle.loads(s2)
%timeit-n100x=json.loads(s3)
100loops,best of3:16.8ms per loop
100loops,best of3:2.02ms per loop
100loops,best of3:798µsper loop

可見json比cPickle快近3倍,比eval快20多倍。

17. 使用C擴展(Extension)

目前主要有CPython(python最常見的實現的方式)原生API, ctypes,Cython,cffi三種方式,它們的作用是使得Python程式可以呼叫由C編譯成的動態鏈接庫,其特點分別是:CPython原生API: 通過引入Python.h頭檔案,對應的C程式中可以直接使用Python的資料結構。實現過程相對繁瑣,但是有比較大的適用範圍。ctypes: 通常用於封裝(wrap)C程式,讓純Python程式呼叫動態鏈接庫(Windows中的dll或Unix中的so檔案)中的函式。如果想要在python中使用已經有C類庫,使用ctypes是很好的選擇,有一些基準測試下,python2+ctypes是性能最好的方式。Cython: Cython是CPython的超集,用於簡化編寫C擴展的過程。Cython的優點是語法簡潔,可以很好地兼容numpy等包含大量C擴展的庫。Cython的使得場景一般是針對專案中某個演算法或過程的優化。在某些測試中,可以有幾百倍的性能提升。cffi: cffi的就是ctypes在pypy(詳見下文)中的實現,同進也兼容CPython。cffi提供了在python使用C類庫的方式,可以直接在python代碼中編寫C代碼,同時支持鏈接到已有的C類庫。使用這些優化方式一般是針對已有專案性能瓶頸模塊的優化,可以在少量改動原有專案的情況下大幅度地提高整個程式的運行效率。

18. 並行編程

因為GIL的存在,Python很難充分利用多核CPU的優勢。但是,可以通過內置的模塊multiprocessing實現下麵幾種並行樣式:多行程:對於CPU密集型的程式,可以使用multiprocessing的Process,Pool等封裝好的類,通過多行程的方式實現並行計算。但是因為行程中的通信成本比較大,對於行程之間需要大量資料交互的程式效率未必有大的提高。多執行緒:對於IO密集型的程式,multiprocessing.dummy模塊使用multiprocessing的接口封裝threading,使得多執行緒編程也變得非常輕鬆(比如可以使用Pool的map接口,簡潔高效)。分佈式:multiprocessing中的Managers類提供了可以在不同行程之共享資料的方式,可以在此基礎上開發出分佈式的程式。不同的業務場景可以選擇其中的一種或幾種的組合實現程式性能的優化。

19. 終級大殺器:PyPy

PyPy是用RPython(CPython的子集)實現的Python,根據官網的基準測試資料,它比CPython實現的Python要快6倍以上。快的原因是使用了Just-in-Time(JIT)編譯器,即動態編譯器,與靜態編譯器(如gcc,javac等)不同,它是利用程式運行的過程的資料進行優化。由於歷史原因,目前pypy中還保留著GIL,不過正在進行的STM專案試圖將PyPy變成沒有GIL的Python。如果python程式中含有C擴展(非cffi的方式),JIT的優化效果會大打折扣,甚至比CPython慢(比Numpy)。所以在PyPy中最好用純Python或使用cffi擴展。隨著STM,Numpy等專案的完善,相信PyPy將會替代CPython。

20. 使用性能分析工具

除了上面在ipython使用到的timeit模塊,還有cProfile。cProfile的使用方式也非常簡單: python -m cProfile filename.py,filename.py 是要運行程式的檔案名,可以在標準輸出中看到每一個函式被呼叫的次數和運行的時間,從而找到程式的性能瓶頸,然後可以有針對性地優化。

《Linux雲計算及運維架構師高薪實戰班》2018年09月16日即將開課中,120天衝擊Linux運維年薪30萬,改變速約~~~~

    *宣告:推送內容及圖片來源於網絡,部分內容會有所改動,版權歸原作者所有,如來源信息有誤或侵犯權益,請聯繫我們刪除或授權事宜。

    – END –


    更多Linux好文請點擊【閱讀原文】

    ↓↓↓

    赞(0)

    分享創造快樂