提高Python性能的一些建議

    最近換住的地方,網費到期,有兩個星期沒更新博客了,博客還是要堅持寫的,有時候工作時遇到了相關問題,查看相關博客,還是能夠得到一些思路或者靈感。雖然寫篇博客要話費不少時間(我一般要花一個半小時到兩個小時之間),但是這中間碼字呀、歸納總結的過程還是讓我受益匪淺的,溫故而知新!當然分享自己的學習心得,也會讓自己認識一些志同道合的朋友,也挺好。不說許多,今天講講如何提高Python性能的問題。

    python的性能相對c語言等還是有一定的劣勢,但是如果能掌握一些優化性能的技巧,不僅能夠提高代碼的運行效率,還能夠使代碼更加Pythonic。剛剛接觸Python時,也在網上找了一些提高`python性能的博文看,還是另外受益匪淺,http://www.jb51.net/article/56699.htm

這篇博文還是寫的不錯的,可以參考。不過結合我自己最近看的書及書上的建議,做一個更細緻的總結吧,不正之處歡迎批評指正,共同進步!

一、循環優化的基本技巧

    循環的優化應遵循儘量減少循環過程中計算量的原則,多重循環的情況下儘量將內存的計算提到上一層。

(1)減少循環內部計算。先看下面例子

#coding=utf-8
import datetime
import math

#第一種方法
def fun1(iter,x):
    j = 0
    for i in iter:
        d = math.sqrt(x)
        j += i * d
    return j

#第二中方法
def fun2(iter,x):
    j = 0
    d = math.sqrt(x)
    for i in iter:
        j += i * d
    return j

iter = range(1,1000)
t0 = datetime.datetime.now()
a = fun1(iter,9)
t1 = datetime.datetime.now()
b = fun2(iter,9)
t2 = datetime.datetime.now()
print a,"  ",b
print t1-t0,t2-t1

    運行結果如下圖:

wKioL1fx55qz_JUKAAApTCz5FSM590.png-wh_50

第二種方法比一種速度快,因爲第一種方法中,d = math.sqrt(x)在循環內部,每次循環過程中都會重複計算一次,增加了系統開銷,這裏的d = math.sqrt(x)還是個比較簡單的計算,如果遇到自己定義的複雜的、計算量大的那麼第一種方法真的就不太好了。一般情況下,第二種方法比第一種方法運算效率高40%-60%。

(2)將顯示循環改爲隱式循環。比如說,在求等差數列的和時,可以直接通過循環來計算,這個很簡單,如下:

#coding=utf-8
def SUM(n):
sum = 0
for i in xrange(n+1):
    sum +=i

這麼寫是沒問題的,但是等差數列有現成的求和公式呀,即n*(n+1)/2,沒必要要再用循環了,所以說如果程序中有類似的情況,可以直接將顯示循環改爲隱式。

(3)在循環中儘量引用局部變量。根據"LEGB"原則(不瞭解的可以參考之前的博文,地址:http://11026142.blog.51cto.com/11016142/1840128  ),在命名空間局部變量優先搜索,因此局部變量查詢會比全局變量更快。在循環中,如果多次引用某一變量,要儘量將其轉化爲局部變量,看下面例子

#coding=utf-8
import datetime
import math
x = [10,20,30,40,50,60,70,80,90]
#第一種方法
def fun1(x):
    for i in xrange(len(x)):
        x[i] = math.sin(x[i])
    return x

#第二中方法
def fun2(x):
    loc_sin = math.sin
    for i in xrange(len(x)):
        x[i] = loc_sin(x[i])
    return x

t0 = datetime.datetime.now()
a = fun1(x)
t1 = datetime.datetime.now()
b = fun2(x)
t2 = datetime.datetime.now()
print a
print b
print t1-t0,t2-t1

運行結果如下:

wKioL1fx7P2g4oXpAAAxhk6B-vg291.png-wh_50

可以看到方法二快於方法一,我覺得這種方法,在平時應用某個庫(包括標準庫和自定義庫,模塊)的函數時,可以這樣用,比較程序搜索這個庫的函數也是需要時間,如果用第一種方法,那就是循環搜索了,那肯定會劃分更多的時間,這個技巧我覺得值得大家去學習、借鑑。

(4)對於嵌套循環,儘量將內層循環計算往上層移。看下面例子

#coding=utf-8
import datetime

#第一種方法
def fun1(iter1,iter2):
    max1 = 0
    for i in range(len(iter1)):
        for j in range(len(iter2)):
            x = iter1[i] + iter2[j]
            max1 = x if x > max1 else max1
    return max1

#第二中方法
def fun2(iter1,iter2):
    max1 = 0
    for i in range(len(iter1)):
        temp = iter1[i]
        for j in range(len(iter2)):
            x = temp + iter2[j]
            max1 = x if x > max1 else max1
    return max1
l1 = [1,23,4,5,34,8,10,18,42,10,6,88]
l2 = [100,102,34,15,16,56]
t0 = datetime.datetime.now()
a = fun1(l1,l2)
t1 = datetime.datetime.now()
b = fun2(l1,l2)
t2 = datetime.datetime.now()
print a
print b
print t1-t0,t2-t1

運行結果如下:

wKiom1fx8e-zVz7QAAAacLt41Tk666.png-wh_50

可見方法二的速度要快些,嵌套for循環的運行機制是i=0(以上面例子爲例),然後j從0增到最大值,然後i自增1,j又從0增大到最大值,依次類推。所以在內層循環的上一層加個臨時變量,內層使用是就不用重新計算。這裏要說明一下,對於列表它的索引,切片等操作也是一種計算/運算,也是要花費時間的。


二、使用不同的數據結構優化性能

    最常用的數據結構是list,它的內存管理類似於c++的vector,即先預分配一定數量的內存,當預分配的內存用完了但是不夠用,又要繼續往裏插入元素,就會啓動新一輪內存分配,list對象就會根據內存增長算法重新申請一塊更大的內存空間,然後將原有的所有元素拷貝過去,銷燬之前的內存,然後插入新元素,如果還不夠用,繼續重複上面的步驟。刪除元素也是這樣,如果發現已用空間比預分配內存空間的一半還少,list會申請一塊小內存,再做一次拷貝,然後銷燬大內存(如果想了解這部分知識,我推薦大家看看《Python源碼剖析》這本書,這本書我也是一個月前開始看,目前還沒看完,我覺得這本書寫的很好,很值得讀兩三篇甚至四五篇,第一章與第二章內容要認真讀,否則後面的東西越看越糊塗(大神除外))。

    可見,如果list對象經常有元素數量的巨大變化,而且比較頻繁,這個時候應該考慮使用deque。如果不瞭解deque,可以參考我前面的博文http://11026142.blog.51cto.com/11016142/1851791


     deque是雙端隊列,同時具備棧和隊列的特性。能夠提供複雜度爲O(1)的兩端插入和刪除操作。相對於list,它最大的優勢在於內存管理。它申請的內存不夠用時,不會像list那樣,而是申請新的內存來容納新的元素,然後將新元素與舊元素連接起來,避免了元素的拷貝。所以,但出現元素數量頻繁出現巨大變化時,deque的性能是list的好幾倍。



    array,中文名是數組,是一種序列數據結構,看起來和list很相似,但是所有成員必須是相同基本類型。array實例化時需要指明其存儲元素類型。如'c',表示存儲一個想當然c語言裏的char類型,佔用內存大小1字節。這就從另一個角度來說明,它可以優化代碼的內存空間。看下面例子

#coding=utf-8
import sys
import array
a = array.array('c',"hello,world")

c = list("hello,world")
print sys.getsizeof(a),sys.getsizeof(c)

運行結果如下:

wKiom1fx_2DBFfuFAAAVEXUmHmo035.png-wh_50

明顯,list對象更耗內存。這就會影響到一些其它操作的性能提升,比如將容器對象轉換爲字符串,在這一點上array性能高於list。看下面例子

#coding=utf-8
import array
import datetime
a = array.array('c',"hello,world")
c = list("hello,world")
t0 = datetime.datetime.now()
s1 = ''.join(c)
t1= datetime.datetime.now()
s2 = ''.join(a)
t2 = datetime.datetime.now()
print t1 - t0,t2 - t1

運行結果如下:

wKioL1fyAcmQ1t7wAAAVZw_2o38190.png-wh_50

也並不是所以的array性能提升比較大,比如排序,array性能不如list,看下例:

#coding=utf-8
import array
import datetime
a = array.array('c',"hello,world")
c = list("hello,world")
t0 = datetime.datetime.now()
c.reverse()
t1= datetime.datetime.now()
a.reverse()
t2 = datetime.datetime.now()
print t1 - t0,t2 - t1

結果如下:

wKiom1fyApLBwFjzAAAVyrI3nDg926.png-wh_50

(三)利用好set的優勢

set是集合,python中集合是通過Hash算法實現的無序不重複元素集,創建集合是通過set()來實現的。看下圖:

wKioL1fyA8fRvBi3AAKqrLC2sho474.png-wh_50

set對象也支持添加元素,但它的性能是list添加元素性能的好幾倍,這個不在此過多敘述,有時間專門寫篇關於python集合的博文。

   set在求交集、並集、差集等與集合有關的操作,性能要逼list快,因此涉及到list的交集、並集、差集等運算,可以將list轉換爲set

    

四、使用生成器提高效率

    生成器是Python中的一個高級用法,概念非常簡單,如果一個函數中有yield語句,則稱爲生成器(generator),它是一種特殊的迭代器(iterator),也可以稱爲可迭代對象(iterable)。這三者關係如下圖:

wKioL1fyWC7S6OCQAADL0VZlqW0392.png-wh_50

如果你熟悉/瞭解Python的對象協議的話,最上面的那個是容器類協議,第二個是迭代器協議,如果你對這些不太熟悉,我覺得有必要找本經典教材來看看,我看的是《Python學習手冊》,這本書還不錯,介紹的比較詳細,對於比較熟的內容,可以跳過。

    調用生成器函數會返回一個迭代器對象,只是這個迭代器對象是以生成器對象的形式出現的。看下面例子:

#coding=utf-8
def fun(n):
    a,b = 1,1
    while a < n:
        yield a
        a,b = b,a + b

f = fun(10)
print type(f)
print dir(f)

輸出結果如下:

<type 'generator'>

['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'next', 'send', 'throw']


可以看到它返回的是一個generator類型對象,而這個對象有__iter__()和__next__()方法,熟悉迭代器協議的都知道,迭代器協議就是要實現__iter__()和__next__()方法,可見它也是一個迭代器對象。

生成器爲什麼能夠提高運行效率?

這是因爲每一個生成器函數在調用之後,它的函數體並不執行,而是第一次調用next()的時候纔會執行,僅在需要的時候產生對應的元素,而不是一次性生成所有的元素,從而節省了空間內存,提高了效率,理論上來講,無限循環成爲可能不會導致內存不夠用的情況,這個在讀數據處理的時候尤爲重要。所以在循環過程中依次處理一個元素的時候,用生成器是最好的。


五、使用多進程

    由於GIT的存在,是的Python中的多線程無法充分利用多核優勢來提高運行效率,但是python提供了另外一個解決方案,多進程。 可以使用multiprocessing的Process,Pool等封裝好的類,通過多進程的方式實現並行計算。由於篇幅有限,在此不過多介紹。



另外還有一些其它的方法,比如推到式啊,藉助一些其它的開發工具呀等等,這也是一個很值得去花時間去了解的知識,由於篇幅有限,無法詳細敘述,後面我會慢慢嘗試使用PyPy,Cython以及一些分析工具,然後將我的學習經驗與大家分享。這些都是一些技巧,平時寫代碼的時候就要注意考慮進這些東西,多實踐多總結,相信你會越來越優秀!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章