1. 程式人生 > >【轉載】python 提高效率的幾個小技巧

【轉載】python 提高效率的幾個小技巧

1.1. 最常見
一個最常見的速度陷坑(至少是俺在沒看到網上這篇介紹時陷進去
過好些次的) 是: 許多短字串併成長字串時, 大家通常會用:

Toggle line numbers
   1 shortStrs = [ str0, str1, ..., strN]
   2 #N+1個字串所組成的數列
   3 longStr = ”
   4 for s in shortStrs: longStr += s
因為Python 裡字串是不可變的, 所以每次 longStr += s 都是將原 來的 longStr 與 str 拷貝成一個新字串, 再賦給longStr. 隨著 longStr的不斷增長, 所要拷貝的內容越來越長. 最後導至str0被 拷貝N+1次, str1是N次, … .

1.1.1. 找出速度瓶頸
1)首先在大家應先學會怎麼去找出速度瓶頸: Python 自帶有profile
模組:

Toggle line numbers
   1 import profile
   2 profile.run (’想要檢查的函式名()’)
就會打印出那個函式裡呼叫了幾次其它函式, 各用了多少時間, 總共用了多少時間等資訊 — Nice ? 詳請參閱<<庫參考>>中的 profile模組的論述.

當然腦袋笨一點或是聰明一點的, 也可以用time模組中的time() 來顯示系統時間, 減去上次的time()就是與它的間隔秒數了.

1.1.2. 字串相併
就頭上的例子而言, 用 :

Toggle line numbers
   1 longStr =”.join(shortStrs)
立馬搞定, 但如果shortStrs裡面不都是字串, 而包含了些數 字呢 ? 直接用join就會出錯. 不怕, 這樣來:

Toggle line numbers
   1 shortStrs = [str(s) for s in shortStrs[i]]
   2 longStr = ”.join(shortStrs)
也即先將數列中所有內容都轉化為字串, 再用join.

對少數幾個字串相併, 應避免用: all = str0 + str1 + str2 + str3 而用: all = ‘%s%s%s%s’ % (str0, str1, str2, str3)

1.1.3. 數列排序
list.sort ()
你可以按特定的函式來: list.sort( 函式 ), 只要這個函式接受 兩引數, 並按特定規則返回1, 0, -1就可以. — 很方便吧? 但 會大大減慢執行速度. 下面的方法, 俺舉例子來說明可能更容易 明白.

比方說你的數列是 l = ['az', 'by'], 你想以第二個字母來排序. 先取出你的關鍵詞, 並與每個字串組成一個元組: new = map (lambda s: (s[1], s), l )

於是new變成[('z', 'az'), ('y', 'by')], 再把new排一下序: new.sort()

則new就變成 [('y', 'by'), ('z', 'az')], 再返回每個元組中 的第二個字串: sorted = map (lambda t: t[1], new)

於是sorted 就是: ['by', 'az']了. 這裡的lambda與map用得很 好.

Python2.4以後, sort和sorted的使用可以參考這片 Wiki: HowToSort

1.1.4. 迴圈
比如for迴圈. 當迴圈體很簡單時, 則迴圈的呼叫前頭(overhead) 會顯得很臃腫, 此時map又可以幫忙了. 比如你想把一個長數列 l=['a', 'b', ...]中的每個字串變成大寫, 可能會用:

Toggle line numbers
   1 import string
   2 newL = []
   3 for s in l: newL.append( string.upper(s) )
用map就可以省去for迴圈的前頭:

Toggle line numbers
   1 import string
   2 newL = map (string.upper, l)
Guido的文章講得很詳細.

1.1.5. 局域變數 及 ‘.’
象上面, 若用 append = newL.append, 及換種import方法:

Toggle line numbers
   1 import string
   2 append = newL.append
   3 for s in l: append (string.upper(s))
會比在for中執行newL.append快一些, 為啥? 局域變數容易尋找.

俺自己就不比較時間了, Skip Montanaro的結果是:

基本迴圈: 3.47秒
去點用局域變數: 1.79秒
使用map: 0.54秒

1.1.6. try的使用
比如你想計算一個字串數列: l = ['I', 'You', 'Python ', 'Perl', ...] 中每個詞出現的次數, 你可能會:

Toggle line numbers
   1 count = {}
   2 for s in l:
   3     if not count.has_key(s): count[s] = 0
   4     else: count[s] += 1
由於每次都得在count中尋找是否已有同名關鍵詞, 會很費時間. 而用try:

Toggle line numbers
   1 count ={}
   2 for s in l:
   3     try: count[s] += 1
   4     except KeyError: count[s] = 0
就好得多. 當然若經常出現例外時, 就不要用try了.

1.1.7. import語句
這好理解. 就是避免在函式定義中來import一個模組, 應全在 全域性塊中來import

1.1.8. 大量資料處理
由於Python 中的函式呼叫前頭(overhead)比較重, 所以處理大量 資料時, 應:

Toggle line numbers
   1 def f():
   2 for d in hugeData: …
   3 f()
而不要:

Toggle line numbers
   1 def f(d): …
   2 for d in hugeData: f(d)
這點好象對其它語言也適用, 差不多是放之四海而皆準, 不過對 解釋性語言就更重要了.

1.1.9. 減少週期性檢查
這是Python 的本徵功能: 週期性檢查有沒有其它緒(thread)或系 統訊號(signal)等要處理.

可以用sys模組中的setcheckinterval 來設定每次檢查的時間間隔.

預設是10, 即每10個虛擬指令 (virtual instruction)檢查一次.

當你不用緒並且也懶得搭理 系統訊號時, 將檢查週期設長會增加速度, 有時還會很顯著.

—編/譯完畢. 看來Python 是易學難精了, 象圍棋?

2. 我們自個兒的體悟
請有心得者分享!

在“大量資料處理”小節裡,是不是說,不要再迴圈體內部呼叫函式,應該把函式放到外面?從Python2.2開始,”找出速度瓶頸”,已經可以使用hotshot模組了.據說對程式執行效率的影響要比profile小. — jacobfan
“由於Python 中的函式呼叫前頭(overhead)比較重, 所以處理大量 資料時, 應: ” 這句譯文中,overhead翻譯成”前頭”好象不妥.翻譯成”由於Python 中函式呼叫的開銷比較大,…”要好些 — jacobfan
陣列排序中講的方法真的會快點嗎? 真的快到我們值得放棄直接用sort得到得可讀性嗎?值得懷疑 — hoxide
Python2.4以後 sort和sorted的使用更加靈活,link已經加到文中,我沒有比較過效率。-yichun
關於 “try的使用”:
其實setdefault方法就是為這個目的設的:

Toggle line numbers
   1 count = {}
   2 for s in l:
   3     count.setdefault(s, 0) += 1
這個其實能做更多。通常遇到的問題是要把類似的東西group起來,所以你可能想用:

Toggle line numbers
   1 count = {}
   2 for s in l:
   3     count.setdefault(s, []).append(s)
但是這樣你只能把同樣的東西hash起來,而不是一類東西。比如說你有一個dict構成的list叫sequence,需要按這些dict的某個key value分類,你還要對分類後的每個類別裡面的這些dict各作一定的操作,你就需要用到Raymond實現的這個groupby,你就可以寫:

totals = dict((key, group)
                  for key, group in groupby(sequence, lambda x: x.get(’Age’)))

說明:增加程式碼的描述力,可以成倍減少你的LOC,做到簡單,並且真切有力
觀點:少打字=多思考+少出錯,10程式碼行比50行更能讓人明白,以下技巧有助於提高5倍工作效率 

1. 交換變數值時避免使用臨時變數:(cookbook1.1)
老程式碼:我們經常很熟練於下面的程式碼
temp = x
x = y
y = temp
程式碼一:
u, v, w = w, v, u  
有人提出可以利用賦值順序來簡化上面的三行程式碼成一行
程式碼二:
u, v = v, u  
其實利用Python元組賦值的概念,可更簡明 -- 元組初始化 + 元組賦值

2. 讀字典時避免判斷鍵值是否存在:(cookbook1.2)
d = { 'key': 'value' }
老程式碼:
if 'key' in d: print d['key']
else: print 'not find'
新程式碼:
print d.get('key', 'not find')   

3. 尋找最小值和位置的程式碼優化
s = [ 4,1,8,3 ]
老程式碼:
mval, mpos = MAX, 0
for i in xrange(len(s)): 
    if s[i ] 
觀點一:用Python程式設計,需要有“一字千金”的感覺;既然選擇了Python,就不要在意單條語句的效率。
上面幾點例子很基礎,實際中將原始程式碼壓縮1/5並不是不可能,我們之前一個子專案,C++程式碼270K,重構後Python程式碼只有67K,當然使用python的日誌模組(logging),讀寫表格文字(csv)等,也功不可末,最終程式碼變成原來的1/4,我覺得自己的壽命延長了三倍。。。下面優化幾個常用程式碼:

4. 檔案讀取工作的最簡單表達:
老程式碼:我們需要將文字檔案讀入到記憶體中
line = ''
fp = open('text.txt', 'r')
for line in fp: text += line
程式碼一:
text = string.join([ line for line in open('text.txt')], ''] 
程式碼二:
text = ''.join([ line for line in open('text.txt')])    
程式碼三:
text = file('text.txt').read()  
新版本的Python可以讓你寫出比1,2漂亮的程式碼(open是file的別名,這裡file更直觀)

5. 如何在Python實現三元式:
老程式碼:用慣C++,Java,C#不喜歡寫下面程式碼
if n >= 0: print 'positive'
else: print 'negitive'
程式碼一:該技巧在 Lua裡也很常見
print (n >= 0) and 'positive' or 'negitive'
說明:這裡的'and'和'or'相當於C中的':'和'?'的作用,道理很簡單,因為如果表示式為真了那麼後面的or被短路,取到'positive';否則,and被短路,取到'negitive'
程式碼二:
print (n >= 0 and ['positive'] or ['negitive])[0]
說明:將兩個值組裝成元組,即使'positive'是None, '', 0 之類整句話都很安全
程式碼三:
print ('negitive', 'positive')[n >= 0]
說明:(FalseValue, TrueValue)[Condition] 是利用了 元組訪問 + True=1 兩條原理

6. 避免字典成員是複雜物件的初始化:(cookbook1.5)
老程式碼:
if not y in d: d[y] = { }
d[y][x] = 3
新程式碼:
d.setdefault(y, { })[x] = 3
如果成員是列表的話也一樣: d.setdefault(key, []).append(val)
上面六點技巧加以發揮,程式碼已經很緊湊了,但是還沒有做到“沒有一句廢話”可能有人懷疑真的能減少1/5的程式碼麼??我要說的是1/5其實很保守,Thinking in C++的作者後來用了Python以後覺得Python甚至提高了10倍的工作效率。下面的例子可以進一步說明:
例子1:把文字的IP地址轉化為整數
說明:需要將類似'192.168.10.214'的IP地址轉化為 0x0C0A80AD6,在不用 inet_aton情況下。當C++/Java程式員正為如何進行文字分析,處理各種錯誤輸入煩惱時,Python程式設計師已經下班:
f = lambda ip: sum( [ int(k)*v for k, v in zip(ip.split('.'), [1
首先ip.split('.')得到列表['192','168','10','214'],經過zip一組裝,就變成
[('192',0x1000000),('168',0x10000),('10',0x100),('214',1)] 
接著for迴圈將各個元組的兩項做整數乘法,最後將新列表的值用sum求和,得到結果
C++程式設計師不肖道:“你似乎太相信資料了,根本沒有考慮道錯誤的輸入”
Python程式設計師回答:“外面的try/except已幫我完成所有異常處理,不必擔心越界崩潰而無法捕獲”
Java程式設計師得意的看著自己百行程式碼:“我想知道你如何讓你的同事來理解你的傑作?你有沒有考慮過將類似gettoken之類的功能獨立處理,讓類似問題可以複用?我的程式碼說明了如何充分發揮Reflection和interface的優秀特性,在增加重用性的同時,提供清晰可讀的程式碼”
Python無奈道:“這是‘純粹的程式碼’,意思是不可修改,類似正則表示式,只要讓人明白他的功能就行了,要修改就重寫。再我能用三行程式碼完成以內絕不會有封裝的想法,況且熟悉Python者也不覺得難讀啊?”
C++程式設計師丟擲殺手簡:“如果讓你一秒鐘處理10w個ip轉化的話怎麼辦?”
Python程式設計師覺得想睡覺:“你覺得我會蠢到還用Python做這樣的事情麼?”
此時C++程式設計師似乎並沒聽到,反而開始認真的思考起自己剛才提出問題來,一會只見他輕藐的看了另外兩人一眼,然後胸有成竹的轉到電腦前,開始往螢幕上輸入: “template <....”
小笑話:封裝的陷阱,讓人一邊喊著“封裝”或“複用”,一邊在新專案中,全部打破重寫,並解釋為--重構
觀點二:簡單即是美,把一個東西設計複雜了,本身就是有問題的
思考題:上面的程式,如果反過來,將ip的整數形式轉化為字串,各位該如何設計呢??
例子2:輸出一個物件各個成員的名稱和值
g = lambda m: '\n'.join([ '%s=%s'%(k, repr(v)) for k, v in m.__dict__.iteritems() ])
用法:print g(x)
延伸:上面兩個例子熟悉了lambda以後,建議可以嘗試使用下 yield

觀點總結
Q:“怎樣才算做到注重What you think多於What you are writing”
A:“就是說你手上打著第1頁需求的程式碼,眼睛卻在看著第2頁需求的內容,心裡想著如何應對5-10頁的東西”
國外多年前廢除PASCAL改用Python做科研教學是有道理的,關於精簡程式碼的例子舉不勝舉,用它編碼時應該有“一字千金”的感覺,否則最終寫出來的,還是“偽裝成Python的C++程式”。
程式設計本來就是快樂的,避免過多的體力勞動,贏得更多思考的時間。
思考題:到底是封裝呢?還是放棄封裝?
思考題:“more than one way to do it”是不是就是好事?它的反面是什麼?