9 個小技巧,加速 Python 的優化思路
Python 是一種腳本語言,相比 C/C++ 這樣的編譯語言,在效率和性能方面存在一些不足。但是,有很多時候,Python 的效率并沒有想象中的那么夸張。本文對一些 Python 代碼加速運行的技巧進行整理。
0. 代碼優化原則
1. 避免全局變量
許多程序員剛開始會用 Python 語言寫一些簡單的腳本,當編寫腳本時,通常習慣了直接將其寫為全局變量,例如上面的代碼。但是,由于全局變量和局部變量實現方式不同,定義在全局范圍內的代碼運行速度會比定義在函數中的慢不少。通過將腳本語句放入到函數中,通??蓭?15% - 30% 的速度提升。
2. 避免模塊和函數屬性訪問
每次使用.
(屬性訪問操作符時)會觸發特定的方法,如__getattribute__()
和__getattr__()
,這些方法會進行字典操作,因此會帶來額外的時間開銷。通過from import
語句,可以消除屬性訪問。
在第 1 節中我們講到,局部變量的查找會比全局變量更快,因此對于頻繁訪問的變量sqrt
,通過將其改為局部變量可以加速運行。
除了math.sqrt
外,computeSqrt
函數中還有.
的存在,那就是調用list
的append
方法。通過將該方法賦值給一個局部變量,可以徹底消除computeSqrt
函數中for
循環內部的.
使用。
3. 避免類內屬性訪問
避免.
的原則也適用于類內屬性,訪問self._value
的速度會比訪問一個局部變量更慢一些。通過將需要頻繁訪問的類內屬性賦值給一個局部變量,可以提升代碼運行速度。
4. 避免不必要的抽象
任何時候當你使用額外的處理層(比如裝飾器、屬性訪問、描述器)去包裝代碼時,都會讓代碼變慢。大部分情況下,需要重新進行審視使用屬性訪問器的定義是否有必要,使用getter/setter
函數對屬性進行訪問通常是 C/C++ 程序員遺留下來的代碼風格。如果真的沒有必要,就使用簡單屬性。
5. 避免數據復制
5.1 避免無意義的數據復制
上面的代碼中value_list
完全沒有必要,這會創建不必要的數據結構或復制。
另外一種情況是對 Python 的數據共享機制過于偏執,并沒有很好地理解或信任 Python 的內存模型,濫用?copy.deepcopy()
之類的函數。通常在這些代碼中是可以去掉復制操作的。
5.2 交換值時不使用中間變量
上面的代碼在交換值時創建了一個臨時變量temp
,如果不借助中間變量,代碼更為簡潔、且運行速度更快。
5.3 字符串拼接用join而不是+
當使用a + b
拼接字符串時,由于 Python 中字符串是不可變對象,其會申請一塊內存空間,將a
和b
分別復制到該新申請的內存空間中。因此,如果要拼接 n?個字符串,會產生 n-1?個中間結果,每產生一個中間結果都需要申請和復制一次內存,嚴重影響運行效率。而使用join()
拼接字符串時,會首先計算出需要申請的總的內存空間,然后一次性地申請所需內存,并將每個字符串元素復制到該內存中去。
6. 利用if條件的短路特性
if
?條件的短路特性是指對if a and b
這樣的語句, 當a
為False
時將直接返回,不再計算b
;對于if a or b
這樣的語句,當a
為True
時將直接返回,不再計算b
。因此, 為了節約運行時間,對于or
語句,應該將值為True
可能性比較高的變量寫在or
前,而and
應該推后。
7. 循環優化
7.1 用for循環代替while循環
Python 的for
循環比while
循環快不少。
7.2 使用隱式for循環代替顯式for循環
針對上面的例子,更進一步可以用隱式for
循環來替代顯式for
循環
7.3 減少內層for循環的計算
上面的代碼中sqrt(x)
位于內側for
循環, 每次訓練過程中都會重新計算一次,增加了時間開銷。
8. 使用numba.jit
我們沿用上面介紹過的例子,在此基礎上使用numba.jit
。numba
可以將 Python 函數 JIT 編譯為機器碼執行,大大提高代碼運行速度。關于numba
的更多信息見下面的主頁:http://numba.pydata.org/numba.pydata.org
9. 選擇合適的數據結構
Python 內置的數據結構如str
,?tuple
,?list
,?set
,?dict
底層都是 C 實現的,速度非??欤约簩崿F新的數據結構想在性能上達到內置的速度幾乎是不可能的。
list
類似于 C++ 中的std::vector
,是一種動態數組。其會預分配一定內存空間,當預分配的內存空間用完,又繼續向其中添加元素時,會申請一塊更大的內存空間,然后將原有的所有元素都復制過去,之后銷毀之前的內存空間,再插入新元素。
刪除元素時操作類似,當已使用內存空間比預分配內存空間的一半還少時,會另外申請一塊小內存,做一次元素復制,之后銷毀原有大內存空間。
因此,如果有頻繁的新增、刪除操作,新增、刪除的元素數量又很多時,list的效率不高。此時,應該考慮使用collections.deque
。collections.deque
是雙端隊列,同時具備棧和隊列的特性,能夠在兩端進行 O(1) 復雜度的插入和刪除操作。
list
的查找操作也非常耗時。當需要在list
頻繁查找某些元素,或頻繁有序訪問這些元素時,可以使用bisect
維護list
對象有序并在其中進行二分查找,提升查找的效率。
另外一個常見需求是查找極小值或極大值,此時可以使用heapq
模塊將list
轉化為一個堆,使得獲取最小值的時間復雜度是 O(1)。
下面的網頁給出了常用的 Python 數據結構的各項操作的時間復雜度:https://wiki.python.org/moin/TimeComplexity
參考資料
-
David Beazley & Brian K. Jones. Python Cookbook, Third edition. O'Reilly Media, ISBN: 9781449340377, 2013. -
張穎 & 賴勇浩. 編寫高質量代碼:改善Python程序的91個建議. 機械工業出版社, ISBN: 9787111467045, 2014.
作者:張皓(侵刪)
鏈接:https://zhuanlan.zhihu.com/p/143052860