從本文總結了Python開發時,遇到的效能最佳化問題的定位和解決。
概述:
效能最佳化的原則——最佳化
需要最佳化的部分
。
效能最佳化的一般步驟:
首先,讓你的程式跑起來結果一切正常。然後,執行這個結果正常的程式碼,看看它是不是真的很慢。第三,如果很慢,找出佔用大部分時間的程式碼。一個全面的測試用例可以保證未來的最佳化不會改變你程式的正確性。簡單說,就是:
寫程式碼
檢查程式碼執行結果是否正確
分析慢不慢
最佳化
返回第二步繼續
某些最佳化等同於好的程式設計風格,這些應該在你學程式語言的時候就學會,比如,把那些迴圈內不會改變的值的計算過程移動到迴圈外。
1、使用列表生成式——簡潔快速生成新列表
老程式碼:
cube_numbers = [] for n in range(0,10): if n % 2 == 1: cube_numbers。append(n**3)
新程式碼:
cube_numbers = [n**3 for n in range(1,10) if n%2 == 1]
在程式碼量較少的時候,這兩種方法可能差不多,但程式碼量多一些,可就不一樣了。
2、儘量使用內建的方法
Python有很多內建的方法,你可以寫高質量、高效的程式碼,但這也很難打敗內建的庫。這些程式碼已經被最佳化和嚴格測試過,檢視內建方法列表,看看你是否重複造輪子了。
3、使用xrange()而不是range()
在Python2中,使用xrange()而非range()可以避免在迴圈中,在記憶體中儲存所有數字,xrange()返回的是一個生成器,當迴圈這個物件時,在記憶體中僅僅儲存當前物件。
想檢視一個物件的記憶體佔用,可以使用:
import sysnumbers = range(1,10000)print(sys。getsizeof(numbers))
在Python3中使用range(),相當於Python2中的xrange()
4、考慮自己寫生成器
前面幾點提到了一般的最佳化模式,即,生成器能用就用。生成器允許我們一次返回一個物件,而不是所有物件。如前所述,xrange()正是一個Python2中實現的生成器,Python3中的range()也是生成器。
如果你工作中使用列表,考慮寫自己的生成器,以使用這種延遲載入和高效的記憶體利用方法。生成器在讀大量檔案時尤其有用,處理大塊檔案而不必擔心其大小,因為生成器的存在而成為可能。
import requestsimport redef get_pages(link): pages_to_visit = [] pages_to_visit。append(link) pattern = re。compile(‘https?’) while pages_to_visit: current_page = pages_to_visit。pop(0) page = requests。get(current_page) for url in re。findall(‘’, str(page。content)): if url[0] == ‘/’: url = current_page + url[1:] if pattern。match(url): pages_to_visit。append(url) yield current_pagewebpage = get_pages(‘http://www。example。com’)for result in webpage: print(result)
該例子每次只返回一個頁面並執行某種操作,在上述程式碼中,是列印連結。
如果沒有生成器,則需要在開始處理之前,同時獲取、處理或者收集所有連結。這樣的程式碼更乾淨,更快,更容易測試。
5、檢查元素存在儘可能用in
檢查列表中的成員,使用in更快一些。
for name in member_list: print(‘{} is a member’。format(name))
6、區域性匯入模組
最開始學Python時,我們可能習慣於在程式碼最前面匯入所有我們想使用的模組,甚至用字母順序排序。。。這種方式讓你可以輕鬆地看到你的程式碼用了哪些模組,但是,壞處是你所有的匯入都在最開始被載入了。(此處不太贊同原文中說的,還是要視情況而定,如果頻繁呼叫的方法,在方法內部區域性匯入,豈不是重複載入)
原文對這種方法的好處的解釋是:該做法有助於均勻的分配模組的載入時間,可以減少記憶體使用量的峰值。
還是那句話,視情況而定。
7、使用集合
過多的迴圈會給伺服器帶來不必要的壓力。假設你想得到在兩個列表中的相同的值,你可以使用多重迴圈,像這樣:
a = [1,2,3,4,5]b = [2,3,4,5,6]overlaps = []for x in a: for y in b: if x==y: overlaps。append(x)print(overlaps)
這個程式碼可以輸出正確結果,但時間複雜度是O(n^2) ,可以使用如下程式碼替換:
print(set(a) & set(b))
集合是利用Hash演算法實現的無序不重複元素集。涉及到如上的,對list求交、並、差、異或,可以轉換為set進行操作,如下:
s。union(t): s&t, 平均時間複雜度:O(len(s)+ len(t))
s。intersection(t): s|t 最差時間複雜度同上
s。difference(t) s-t 平均時間複雜度為O(len(s))
s。symmetric_difference(t) 平均時間複雜度O(len(s)),最差時間複雜度為O(len(s)*len(t))
使用set代替使用list進行運算,速度和記憶體佔用都得到很大的提升。
8、變數賦值
使用如下方式,優雅的賦值:
first_name, last_name, city = ”Kevin“, ”Cunningham“, ”Brighton“
交換兩個變數的值,你可以:
x, y = y, x
比下面的這種方式,既優雅,記憶體佔用又少。
temp = x x = yy = temp
9、避免使用全域性變數
儘量不使用全域性變數是一種有效的設計模式,這是因為這樣做可以保持對作用域的跟蹤,防止不必要的記憶體使用。而且,Python檢索區域性變數比全域性變數更快,所以請儘可能避免使用全域性關鍵字。
10、使用join()連線字串
在Python中,字串是不可變型別,你可以使用+來連線字串,但是+操作每次都要建立新字串並且複製舊的內容過去。一個有效的方法是,使用陣列array模組修改單個字元,然後使用join來重新建立結果字串。
+方法:
new = ”This“ + ”is“ + ”going“ + ”to“ + ”require“ + ”a“ + ”new“ + ”string“ + ”for“ + ”every“ + ”word“print(new)
得到:
Thisisgoingtorequireanewstringforeveryword
而使用join:
new = ” “。join([”This“, ”will“, ”only“, ”create“, ”one“, ”string“, ”and“, ”we“, ”can“, ”add“, ”spaces。“])print(new)
得到:
This will only create one string and we can add spaces。
可以看出,使用join()拼接字串更優雅,更快速
11、保持Python的版本更新
Python的維護者對於使Python更快,魯棒性更強有很大的熱情。一般來說,每一個新版本都會提升Python的效能和安全性。但是,要保證你所用的庫在新版Python上也能用。
12、無限迴圈中,用 while 1
如果你在聽socket,那麼你可能想使用無限迴圈。平時大家會用while True, 這有用,但是你可以使用更輕便的 while 1來達到完全相同的效果。
13、換種方式
一旦你在你的應用中使用一種程式設計方式,你可能會複用、再複用這一種方法。但是,多實驗集中不同的方法可以讓你看到哪種實現更好。這不僅會讓你學習和思考你寫的程式碼的方式,而且還會讓你更有創新精神,想想你如何能更有創造性的用新方法來實現更快更穩定的結果。
簡言之:花式寫程式碼,多騷,多測,最後穩如老狗。
14、儘可能早的離開
當你知道某個方法執行到無法再做有意義的工作時,嘗試離開,這樣可以減少縮排,增加可讀性,還避免了巢狀:
老程式碼:
if positive_case: if particular_example: do_somethingelse: raise exception
新程式碼:
if not positive_case: raise exceptionif not particular_example: raise exceptiondo_something
當我們用很多組輸入去測試時,會發現新程式碼中丟擲異常更早,而且你不需要一直梳理這些條件中的邏輯鏈。
15、瞭解itertools
itertools 是個大寶貝。如果你沒聽過,那麼Python的一大部分標準庫你就錯過了。你可以使用itertools中的方法快速、優雅、記憶體高效的建立方法。
好好看看文件,尋找教程以充分利用該庫。(說的我想單獨開一篇文章介紹一下了。。。)
其中一個例子是排列函式,假設你想生成列表[‘a’, ‘c’, ‘b’]的全排列,你可以:
import itertoolsiter = itertools。permutations([”a“, ”c“, ‘b’])list(iter)
試試吧!賊有用,賊快。
16、使用裝飾器快取
記憶化是一種特定的快取型別,可以最佳化軟體的執行速度,一般來說,快取儲存著最近操作的結果,該結果可以被呈現為網頁或者複雜計算的結果。(沒太懂,原文如下)
Memoization is a specific type of caching that optimizes software running speeds。 Basically, a cache stores the results of an operation for later use。 The results could be rendered web pages or the results of complex calculations。
你可以自己嘗試計算第100個Fibonacci數。
舊程式碼:
def fibonacci(n): if n == 0: # There is no 0‘th number return 0 elif n == 1: # We define the first number as 1 return 1 return fibonacci(n - 1) + fibonacci(n-2)
用上述辦法計算fibonacci數時,你的電腦會發出轟鳴聲,尤其是n較大的時候。。。
如果你使用標準庫中的裝飾器快取,就不一樣了:
import functools@functools。lru_cache(maxsize=128)def fibonacci(n): if n == 0: return 0 elif n == 1: return 1 return fibonacci(n - 1) + fibonacci(n-2)
在Python中,裝飾器使用別的方法並且延展其功能。我們使用@symbol這樣的符號來標識使用裝飾器的方法。在上述程式碼中,使用了functools。Iru_cache裝飾器,將執行結果存入記憶體。
還有其他形式的裝飾器,你也可以寫自己的裝飾器,但這個裝飾器快而且是內建的。有多快?自己試試吧。
17、排序時使用keys
文中描述的方法:
import operatormy_list = [(”Josh“, ”Grobin“, ”Singer“), (”Marco“, ”Polo“, ”General“), (”Ada“, ”Lovelace“, ”Scientist“)]my_list。sort(key=operator。itemgetter(0))
事實上我覺得sorted也是很好用的:
sorted_my_list = sorted(my_list, key=lambda x: x[0])
只能說,都可以吧。
18、不要為條件構造一個集合
你有時候是不是想把你的程式碼構建成這樣:
if animal in set(animals):
上面這個主意看起來很合理。把animals裡面的重複資料刪掉,感覺上會更快
if animal in animals:
但是,即使列表中可能有很多項重複,直譯器的最佳化程度仍然很高,以至於先set再檢查in可能會減慢速度。一般來說,不使用set而是用下面的那種,總是更快的。
19、使用連結串列
Python 列表資料型別實現為陣列。這意味著,在列表開頭新增一個元素可能是非常費力的操作,因為每個元素都得移動。連結串列資料型別這下就派上用場了。不同於陣列,連結串列中每一項都有和表中下一項的連線——由此得名。
列表需要實現分配好記憶體,這種分配代價高昂,浪費巨大,如果你提前不知道你要多大的陣列,那更是如此。
連結串列允許你在你需要的時候才分配記憶體,每一項都被存在記憶體的不同部分,連結將這些項連線起來。
連結串列的問題是查詢時間比較慢,需要做個徹底分析,來確定是不是更好的辦法。
20、使用基於雲的Python效能工具
當你在本地工作時,你可以用一些效能工具來定位你的專案的效能瓶頸。如果你的專案將會被部署到網上,就有點不同了。stackify(文章所在的網站,一波軟廣告)將讓你看到你的網站在生產環境下的表現,也會提供程式碼分析,錯誤追蹤,伺服器指標等。
結論:
這些 tips 只是指出你的程式碼可能遇到的陷阱,和一些建議的解決方式,具體怎麼最佳化,還是要你自己去考慮。不管怎麼樣,效能最佳化之路,就從此開始吧。
參考這些包,將程式碼轉換為C或者機器碼:
http://cython。org/
http://www。cosc。canterbury。ac。nz/~greg/python/Pyrex/
http://psyco。sourceforge。net/
http://www。scipy。org/Weave
http://code。google。com/p/shedskin/
http://pyinline。sourceforge。net/