【學習心得】寫出快速python程序【可能是一】
本文的目的是與大家交流分享一些在數據科學領域使Python程序更加快速的心得。
目標人群:
- 具有基礎python程序水平的人。
- 有需要自行編寫一些基本演算法和數據處理,或者需要編寫小規模產品的數據科學家
歡迎CS專家指正和增補
等一下,我是數據科學家為什麼要優化函數?
最暴力的的答案是:當你被dev追殺時候你就知道了。
然後現實的答案是,目前很多公司都需要數據科學家直接做出產品。當你按下回車,你的pipeline跳出一串專門寫給老闆看的」XXX model started"之類廢話之後迅速產生結果時,你老闆的讚許要比你跑一段便秘的程序要多的多。
Seriously……
Python是數據科學家最常用的語言之一,它的適用性可以讓編程新手都能在短時間內實現某些基本的模型和演算法。然而它的最大問題/優點就是沒有static typing。因此每一步的執行都會加入大量的檢查和轉換。比如下面的一段簡單的命令:
test = [] # 在內存中建立一個reference叫做test,它指向一個空的listtest.append(1) # 1. 檢查test這個變數是否有append這個method # 2. 執行append(1)這個命令
看起來似乎不是那麼可怕?這裡只是需要確認test的類型是list
然而再讓我們看看append(1)是怎麼執行的。
在64位系統的python里,一個空的list佔用了72個bytes (Python Memory Management),簡單來說就是它為了方便用戶加入新的項,預留了多餘的內存空間。從而在執行append的時候可能並不增加整個變數的大小。
於是append(1)做的就是,增加一個新的index,它指向了一個整數值。test的長度從0變成了1.這裡進行了兩步運算。然而由於list的項可以是任何類型,在選取之後,python必須每次都進行判斷來進行下一步運算。請看下一段代碼:
foo = test[0] + 3
這裡進行了大量的類型檢查。
- 首先調用了第0位的值,我們知道他現在是1
- 調用 + 這個運算。python必須要知道1是能與其他變數進行+的運算。於是它對1進行了檢查,發現它是一個int類型的變數 (這一步如果是Static typing的話就可以避免),於是調用 + 這個運算,接下來對 3 進行檢查,發現它也是int類型的變數,於是相加得到4,並賦予foo這個新的變數。
這些檢查還算是非常基本的檢查。比如下面的代碼就包含著更多的檢查:
# lst = [[1,2,3], [4,5,6]]for x in lst: print(lst[x])
在此我不做詳細闡述。
簡單總結一下,由於幾乎每一步的python運算都包括大量的檢查, 我們會浪費大量的資源在我們已經確定不需要進行的計算上。同時也間接導致了python的interpreter沒辦法對loop進行優化,這就是所有python用戶都熟知的:loop能避免就避免。這種問題在static typing的語言裡面是很少發生甚至幾乎不會發生的。
進一步地,由於有各種type check導致的大量overhead,即使是用戶設計了最優的演算法,仍然有可能達不到預期的速度。可怕的是,在有些時候,用戶可能為了使用內置的高性能methods,必須選擇次優的演算法。
有什麼方法對python代碼進行優化?
有很多方法。
- 一些小竅門。比如短的loop套在長的loop外面,使用generator來儲存只過一遍的list,等等。
- 演算法理論。拋開python緩慢的循環不說,如何用最佳的演算法實現你的函數。一些簡單的複雜度考慮對於需要提高python水平的數據科學家來說是必須的。
- 使用自帶函數庫。Python的一些自帶庫,包括collections, itertools都有一些非常好的工具。
- 使用專家們開發的庫。眾所周知的numpy和pandas
- cython。真正優化python函數的終極武器,我可能會在後續文章中介紹。
這裡必須要強調的是,不要一上來就直接問:為什麼不多線程計算?多線程計算並沒有讓你的代碼變得更好!你只是調用了閑置資源而已。一個優秀的代碼能讓程序加快幾十到幾百倍,而一般的並行運算只能加快(#CPU)倍。那麼問題又來了,為什麼不用GPU那麼多的核?除非你的演算法是簡單的矩陣及線性運算,否則你幾乎無法把你的函數分派給GPU。
一個簡單的rule of thumb:Python代碼越多的函數,速度越慢。
解釋:你喜歡的所有高速函數,都是只在c上面加了少數幾層wrapper的優化過的函數。
一些基本的優化技巧
【以下的知識可能非常簡單,但是很有可能突然就忘記】
修改list時,除非非常短(~ 3),永遠先預填,再插入
# Goodlst = [None]*10lst[i] = foo(**kwargs)# Badlst = []lst.append(foo(**kwargs)
只過一遍的list請使用generator, 節省內存在處理大文件時候很重要
# Gooddef foo(*args): # your code here yield result# Baddef foo(*args): result = [] # your code to fill up result return result
使用一些內置data structure
# known dict value type (e.g. int):# good:from collections import defaultdictmyDict = defaultdict(int)myDict["mykey"] += 1# badmyDict = dict()myDict["mykey"] = myDict.get("mykey", 0) + 1
直接生成dict而不是填入:
# lst = [1, 2, 3, 4]# good:myDict = {x: None for x in lst}# bad:myDict = {}for i in lst: myDict[i] = None# also bad:myDict = dict(1=None, 2=None, 3=None, 4=None)
使用一些內置函數:
# lst = [1, 2, 3, 4] # good:from itertools import combinationsresult = list(combinations(lst, 2))# bad:result = [(i,j) for i in range(len(lst)) for j in range(len(lst)) if j > i]
Numpy 和 pandas的使用
這個恐怕是DS第一個需要知道的。
一些誤區:
切忌追求one-liner
很多人為了追求所謂的Pythonic,把很多東西塞到一行命令里完成。請記住就算你沒有自行給一個中間變數賦值,不代表解釋器沒有使用中間變數。該算的還是得算,該存的還是得存。one-liner在大多數情況下都不是最快的。
list comprehension
這個是python的一個賣點,然而這並不會比loop快多少,並不是實質的優化。當然在確保可讀性的情況下,list comprehension還是要鼓勵的
numpy一定比自己寫快
有些時候,連續使用numpy的函數和nparray的methods會導致重複計算。簡單的例子,尋找第一個大於3的數值
arr = np.asarray([1,2,3,4,5])arr[np.argmax(arr>3)] # 4# orarr[arr > 3][0] # 4
在某些情況下會比你自己寫loop要慢。
推薦閱讀:
※哪個機器學習演算法能在百年後繼續被使用?-你猜?
※是什麼驅動了Python近些年強力的增長?來自Stack Overflow的分析
※哈佛大學數據科學專業解析
※一行代碼,Pandas秒變分散式,快速處理TB級數據
※聚類演算法第二篇-層次聚類演算法Birch
