我們經常聽到 “Python 太慢了”,“Python 性能不行”這樣的觀點。但是,只要掌握一些編程技巧,就能大幅提升 Python 的運行速度。
今天就讓我們一起來看下讓 Python 性能更高的 9 個小技巧
python學習資料分享(無償):
字符串拼接的技巧
如果有大量字符串等待處理,字符串連接將成為 Python 的瓶頸。
一般來講,Python 中有兩種字符串拼接方式:
- 使用該
join()
函數將字符串列表合并為一個字符串 - 使用
+
or+=
符號將每個字符串加成一個
那么哪種方式更快呢?我們一起來看一下
mylist = ["Yang", "Zhou", "is", "writing"]# Using '+'
def concat_plus():result = ""for word in mylist:result += word + " "return result# Using 'join()'
def concat_join():return " ".join(mylist)# Directly concatenation without the list
def concat_directly():return "Yang" + "Zhou" + "is" + "writing"
import timeitprint(timeit.timeit(concat_plus, number=10000))
# 0.002738415962085128
print(timeit.timeit(concat_join, number=10000))
# 0.0008482920238748193
print(timeit.timeit(concat_directly, number=10000))
# 0.00021425005979835987
如上所示,對于拼接字符串列表, join()
方法比在 for 循環中逐個添加字符串更快。
原因很簡單。一方面,字符串是 Python 中的不可變數據,每個 +=
操作都會導致創建一個新字符串并復制舊字符串,這會導致非常大的開銷。
另一方面,.join()
方法是專門為連接字符串序列而優化的。它預先計算結果字符串的大小,然后一次性構建它。因此,它避免了與循環中 +=
操作相關的開銷,因此速度更快。
但是,我們發現最快其實是直接用 +
拼接字符串,這是因為:
- Python 解釋器可以在編譯時優化字符串的連接,將它們轉換為單個字符串。因為沒有循環迭代或函數調用,所以它是一個非常高效的操作。
- 由于所有字符串在編譯時都是已知的,因此 Python 可以非常快速地執行此操作,比循環中的運行時連接甚至優化
.join()
方法快得多。
總之,如果需要拼接字符串列表,請選擇 join()
;如果直接拼接字符串,只需使用 +
即可。
創建列表的技巧
Python 中創建列表的兩種常見方法是:
- 使用函數
list()
[]
直接使用
我們來看下這兩種方法的性能
import timeitprint(timeit.timeit('[]', number=10 ** 7))
# 0.1368238340364769
print(timeit.timeit(list, number=10 ** 7))
# 0.2958830420393497
結果表明,執行 list()
函數比直接使用 []
要慢。
這是因為 是 []
字面語法( literal syntax ),而 list()
是構造函數調用。毫無疑問,調用函數需要額外的時間。
同理,在創建字典時,我們也應該利用 {}
而不是 dict()
成員關系測試的技巧
成員關系測試的性能很大程度上取決于底層數據結構
import timeitlarge_dataset = range(100000)
search_element = 2077large_list = list(large_dataset)
large_set = set(large_dataset)def list_membership_test():return search_element in large_listdef set_membership_test():return search_element in large_setprint(timeit.timeit(list_membership_test, number=1000))
# 0.01112208398990333
print(timeit.timeit(set_membership_test, number=1000))
# 3.27499583363533e-05
如上面的代碼所示,集合中的成員關系測試比列表中的成員關系測試要快得多。
這是為什么呢?
- 在 Python 列表中,成員關系測試 (
element in list
) 是通過遍歷每個元素來完成的,直到找到所需的元素或到達列表的末尾。因此,此操作的時間復雜度為 O(n)。 - Python 中的集合是作為哈希表實現的。在檢查成員資格 (
element in set
) 時,Python 使用哈希機制,其時間復雜度平均為 O(1)。
這里的技巧重點是在編寫程序時仔細考慮底層數據結構。利用正確的數據結構可以顯著加快我們的代碼速度。
使用推導式而不是 for 循環
Python 中有四種類型的推導式:列表、字典、集合和生成器。它們不僅為創建相對數據結構提供了更簡潔的語法,而且比使用 for 循環具有更好的性能。
因為它們在 Python 的 C 實現中進行了優化。
import timeitdef generate_squares_for_loop():squares = []for i in range(1000):squares.append(i * i)return squaresdef generate_squares_comprehension():return [i * i for i in range(1000)]print(timeit.timeit(generate_squares_for_loop, number=10000))
# 0.2797503340989351
print(timeit.timeit(generate_squares_comprehension, number=10000))
# 0.2364629579242319
上面的代碼是列表推導式和 for 循環之間的簡單速度比較。如結果所示,列表推導式速度更快。
訪問局部變量速度更快
在 Python 中,訪問局部變量比訪問全局變量或對象的屬性更快。
import timeitclass Example:def __init__(self):self.value = 0obj = Example()def test_dot_notation():for _ in range(1000):obj.value += 1def test_local_variable():value = obj.valuefor _ in range(1000):value += 1obj.value = valueprint(timeit.timeit(test_dot_notation, number=1000))
# 0.036605041939765215
print(timeit.timeit(test_local_variable, number=1000))
# 0.024470250005833805
原理也很簡單:當編譯一個函數時,它內部的局部變量是已知的,但其他外部變量需要時間來檢索。
優先考慮內置模塊和庫
當我們討論 Python 的時候,通常指的是 CPython,因為 CPython 是 Python 語言的默認和使用最廣泛的實現。
考慮到它的大多數內置模塊和庫都是用C語言編寫的,C語言是一種更快、更低級的語言,我們應該利用它的內置庫,避免重復造輪子。
import timeit
import random
from collections import Counterdef count_frequency_custom(lst):frequency = {}for item in lst:if item in frequency:frequency[item] += 1else:frequency[item] = 1return frequencydef count_frequency_builtin(lst):return Counter(lst)large_list = [random.randint(0, 100) for _ in range(1000)]print(timeit.timeit(lambda: count_frequency_custom(large_list), number=100))
# 0.005160166998393834
print(timeit.timeit(lambda: count_frequency_builtin(large_list), number=100))
# 0.002444291952997446
上面的程序比較了計算列表中元素頻率的兩種方法。正如我們所看到的,利用 collections
模塊的內置計數器比我們自己編寫 for
循環更快、更簡潔、更好。
使用緩存裝飾器
緩存是避免重復計算和提高程序速度的常用技術。
幸運的是,在大多數情況下,我們不需要編寫自己的緩存處理代碼,因為 Python 提供了一個開箱即用的裝飾器 — @functools.cache
。
例如,以下代碼將執行兩個斐波那契數生成函數,一個具有緩存裝飾器,但另一個沒有:
import timeit
import functoolsdef fibonacci(n):if n in (0, 1):return nreturn fibonacci(n - 1) + fibonacci(n - 2)@functools.cache
def fibonacci_cached(n):if n in (0, 1):return nreturn fibonacci_cached(n - 1) + fibonacci_cached(n - 2)# Test the execution time of each function
print(timeit.timeit(lambda: fibonacci(30), number=1))
# 0.09499712497927248
print(timeit.timeit(lambda: fibonacci_cached(30), number=1))
# 6.458023563027382e-06
可以看到 functools.cache
裝飾器如何使我們的代碼運行得更快。
緩存版本的速度明顯更快,因為它緩存了先前計算的結果。因此,它只計算每個斐波那契數一次,并從緩存中檢索具有相同參數的后續調用
while 1 VS while True
如果要創建無限 while 循環,我們可以使用 while True
or while 1
.
它們的性能差異通常可以忽略不計。但有趣的是, while 1
稍微快一點。
這是因為是 1 字面量,但 True 是一個全局名稱,需要在 Python 的全局作用域中查找。所以 1 的開銷很小。
import timeitdef loop_with_true():i = 0while True:if i >= 1000:breaki += 1def loop_with_one():i = 0while 1:if i >= 1000:breaki += 1print(timeit.timeit(loop_with_true, number=10000))
# 0.1733035419601947
print(timeit.timeit(loop_with_one, number=10000))
# 0.16412191605195403
正如我們所看到的,確實 while 1
稍微快一些。
然而,現代 Python 解釋器(如 CPython )是高度優化的,這種差異通常是微不足道的。所以我們不需要擔心這個可以忽略不計的差異。更不用說 while True
比 while 1
可讀性更好。
按需導入 Python 模塊
在 Python 腳本開頭導入所有模塊似乎是每個人都會這么做的操作,事實上我們沒有必要導入全部的模塊。如果模塊太大,則根據需要導入它是一個更好的主意。
def my_function():import heavy_module# rest of the function
如上面的代碼所示,heavy_module
在函數中導入。這是一種“延遲加載”的思想:只有 my_function
被調用的時候該模塊才會被導入。
這種方法的好處是,如果 my_function
在腳本執行期間從未調用過,則 heavy_module
永遠不會加載,從而節省資源并減少腳本的啟動時間。