最近換住的地方,網費到期,有兩個星期沒更新博客了,博客還是要堅持寫的,有時候工作時遇到了相關問題,查看相關博客,還是能夠得到一些思路或者靈感。雖然寫篇博客要話費不少時間(我一般要花一個半小時到兩個小時之間),但是這中間碼字呀、歸納總結的過程還是讓我受益匪淺的,溫故而知新!當然分享自己的學習心得,也會讓自己認識一些志同道合的朋友,也挺好。不說許多,今天講講如何提高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
運行結果如下圖:
第二種方法比一種速度快,因爲第一種方法中,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
運行結果如下:
可以看到方法二快於方法一,我覺得這種方法,在平時應用某個庫(包括標準庫和自定義庫,模塊)的函數時,可以這樣用,比較程序搜索這個庫的函數也是需要時間,如果用第一種方法,那就是循環搜索了,那肯定會劃分更多的時間,這個技巧我覺得值得大家去學習、借鑑。
(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
運行結果如下:
可見方法二的速度要快些,嵌套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)
運行結果如下:
明顯,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
運行結果如下:
也並不是所以的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
結果如下:
(三)利用好set的優勢
set是集合,python中集合是通過Hash算法實現的無序不重複元素集,創建集合是通過set()來實現的。看下圖:
set對象也支持添加元素,但它的性能是list添加元素性能的好幾倍,這個不在此過多敘述,有時間專門寫篇關於python集合的博文。
set在求交集、並集、差集等與集合有關的操作,性能要逼list快,因此涉及到list的交集、並集、差集等運算,可以將list轉換爲set
四、使用生成器提高效率
生成器是Python中的一個高級用法,概念非常簡單,如果一個函數中有yield語句,則稱爲生成器(generator),它是一種特殊的迭代器(iterator),也可以稱爲可迭代對象(iterable)。這三者關係如下圖:
如果你熟悉/瞭解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以及一些分析工具,然後將我的學習經驗與大家分享。這些都是一些技巧,平時寫代碼的時候就要注意考慮進這些東西,多實踐多總結,相信你會越來越優秀!