Python加速程式執行的方法
問題
你的程式執行太慢,你想在不使用複雜技術比如C擴充套件或JIT編譯器的情況下加快程式執行速度。
解決方案
關於程式優化的第一個準則是“不要優化”,第二個準則是“不要優化那些無關緊要的部分”。 如果你的程式執行緩慢,首先你得使用14.13小節的技術先對它進行效能測試找到問題所在。
通常來講你會發現你得程式在少數幾個熱點位置花費了大量時間, 比如記憶體的資料處理迴圈。一旦你定位到這些點,你就可以使用下面這些實用技術來加速程式執行。
使用函式
很多程式設計師剛開始會使用Python語言寫一些簡單指令碼。 當編寫指令碼的時候,通常習慣了寫毫無結構的程式碼,比如:
# somescript.py import sys import csv with open(sys.argv[1]) as f: for row in csv.reader(f): # Some kind of processing pass
很少有人知道,像這樣定義在全域性範圍的程式碼執行起來要比定義在函式中執行慢的多。 這種速度差異是由於區域性變數和全域性變數的實現方式(使用區域性變數要更快些)。 因此,如果你想讓程式執行更快些,只需要將指令碼語句放入函式中即可:
# somescript.py import sys import csv def main(filename): with open(filename) as f: for row in csv.reader(f): # Some kind of processing pass main(sys.argv[1])
速度的差異取決於實際執行的程式,不過根據經驗,使用函式帶來15-30%的效能提升是很常見的。
儘可能去掉屬性訪問
每一次使用點(.)操作符來訪問屬性的時候會帶來額外的開銷。 它會觸發特定的方法,比如 __getattribute__()
和 __getattr__()
,這些方法會進行字典操作操作。
通常你可以使用 from module import name
這樣的匯入形式,以及使用繫結的方法。 假設你有如下的程式碼片段:
import math def compute_roots(nums): result = [] for n in nums: result.append(math.sqrt(n)) return result # Test nums = range(1000000) for n in range(100): r = compute_roots(nums)
在我們機器上面測試的時候,這個程式花費了大概40秒。現在我們修改 compute_roots()
函式如下:
from math import sqrt def compute_roots(nums): result = [] result_append = result.append for n in nums: result_append(sqrt(n)) return result
修改後的版本執行時間大概是29秒。唯一不同之處就是消除了屬性訪問。 用 sqrt()
代替了 math.sqrt()
。 The result.append()
方法被賦給一個區域性變數 result_append
,然後在內部迴圈中使用它。
不過,這些改變只有在大量重複程式碼中才有意義,比如迴圈。 因此,這些優化也只是在某些特定地方才應該被使用。
理解區域性變數
之前提過,區域性變數會比全域性變數執行速度快。 對於頻繁訪問的名稱,通過將這些名稱變成區域性變數可以加速程式執行。 例如,看下之前對於 compute_roots()
函式進行修改後的版本:
import math def compute_roots(nums): sqrt = math.sqrt result = [] result_append = result.append for n in nums: result_append(sqrt(n)) return result
在這個版本中,sqrt
從 math
模組被拿出並放入了一個區域性變數中。 如果你執行這個程式碼,大概花費25秒(對於之前29秒又是一個改進)。 這個額外的加速原因是因為對於區域性變數 sqrt
的查詢要快於全域性變數 sqrt
對於類中的屬性訪問也同樣適用於這個原理。 通常來講,查詢某個值比如 self.name 會比訪問一個區域性變數要慢一些。 在內部迴圈中,可以將某個需要頻繁訪問的屬性放入到一個區域性變數中。例如:
# Slower class SomeClass: ... def method(self): for x in s: op(self.value) # Faster class SomeClass: ... def method(self): value = self.value for x in s: op(value)
避免不必要的抽象
任何時候當你使用額外的處理層(比如裝飾器、屬性訪問、描述器)去包裝你的程式碼時,都會讓程式執行變慢。 比如看下如下的這個類:
class A: def __init__(self,x,y): self.x = x self.y = y @property def y(self): return self._y @y.setter def y(self,value): self._y = value
現在進行一個簡單測試:
>>> from timeit import timeit >>> a = A(1,2) >>> timeit('a.x','from __main__ import a') 0.07817923510447145 >>> timeit('a.y','from __main__ import a') 0.35766440676525235 >>>
可以看到,訪問屬性y相比屬性x而言慢的不止一點點,大概慢了4.5倍。 如果你在意效能的話,那麼就需要重新審視下對於y的屬性訪問器的定義是否真的有必要了。 如果沒有必要,就使用簡單屬性吧。 如果僅僅是因為其他程式語言需要使用getter/setter函式就去修改程式碼風格,這個真的沒有必要。
使用內建的容器
內建的資料型別比如字串、元組、列表、集合和字典都是使用C來實現的,執行起來非常快。 如果你想自己實現新的資料結構(比如連結列表、平衡樹等), 那麼要想在效能上達到內建的速度幾乎不可能,因此,還是乖乖的使用內建的吧。
避免建立不必要的資料結構或複製
有時候程式設計師想顯擺下,構造一些並沒有必要的資料結構。例如,有人可能會像下面這樣寫:
values = [x for x in sequence] squares = [x*x for x in values]
也許這裡的想法是首先將一些值收集到一個列表中,然後使用列表推導來執行操作。 不過,第一個列表完全沒有必要,可以簡單的像下面這樣寫:
squares = [x*x for x in sequence]
與此相關,還要注意下那些對Python的共享資料機制過於偏執的程式所寫的程式碼。 有些人並沒有很好的理解或信任Python的記憶體模型,濫用 copy.deepcopy()
之類的函式。 通常在這些程式碼中是可以去掉複製操作的。
討論
在優化之前,有必要先研究下使用的演算法。 選擇一個複雜度為 O(n log n) 的演算法要比你去調整一個複雜度為 O(n**2) 的演算法所帶來的效能提升要大得多。
如果你覺得你還是得進行優化,那麼請從整體考慮。 作為一般準則,不要對程式的每一個部分都去優化,因為這些修改會導致程式碼難以閱讀和理解。 你應該專注於優化產生效能瓶頸的地方,比如內部迴圈。
你還要注意微小優化的結果。例如考慮下面建立一個字典的兩種方式:
a = { 'name' : 'AAPL','shares' : 100,'price' : 534.22 } b = dict(name='AAPL',shares=100,price=534.22)
後面一種寫法更簡潔一些(你不需要在關鍵字上輸入引號)。 不過,如果你將這兩個程式碼片段進行效能測試對比時,會發現使用 dict()
的方式會慢了3倍。 看到這個,你是不是有衝動把所有使用 dict()
的程式碼都替換成第一種。 不夠,聰明的程式設計師只會關注他應該關注的地方,比如內部迴圈。在其他地方,這點效能損失沒有什麼影響。
如果你的優化要求比較高,本節的這些簡單技術滿足不了,那麼你可以研究下基於即時編譯(JIT)技術的一些工具。 例如,PyPy工程是Python直譯器的另外一種實現,它會分析你的程式執行並對那些頻繁執行的部分生成本機機器碼。 它有時候能極大的提升效能,通常可以接近C程式碼的速度。 不過可惜的是,到寫這本書為止,PyPy還不能完全支援Python3. 因此,這個是你將來需要去研究的。你還可以考慮下Numba工程, Numba是一個在你使用裝飾器來選擇Python函式進行優化時的動態編譯器。 這些函式會使用LLVM被編譯成本地機器碼。它同樣可以極大的提升效能。 但是,跟PyPy一樣,它對於Python 3的支援現在還停留在實驗階段。
最後我引用John Ousterhout說過的話作為結尾:“最好的效能優化是從不工作到工作狀態的遷移”。 直到你真的需要優化的時候再去考慮它。確保你程式正確的執行通常比讓它執行更快要更重要一些(至少開始是這樣的).
以上就是Python加速程式執行的方法的詳細內容,更多關於Python加速程式執行的資料請關注我們其它相關文章!