掌握這九個技巧,讓Python代碼快如閃電
前言
這種觀點在關(guān)于編程語言的討論中經(jīng)常出現(xiàn),經(jīng)常掩蓋了Python的眾多優(yōu)點。
事實是,如果能以Pythonic的方式編寫Python代碼,它是很快的。
細節(jié)決定成敗。經(jīng)驗豐富的Python開發(fā)者掌握了一系列微妙而強大的技巧,可以顯著提高代碼的性能。
這些技巧乍看之下似乎微不足道,但它們可以帶來效率的大幅提升。讓我們深入了解其中的9種方法,改變編寫和優(yōu)化Python代碼的方式。
1. 更快的字符串連接:巧妙選擇“join()”或“+”
如果有大量字符串等待處理,字符串連接將成為Python程序的瓶頸。
基本上,Python有兩種字符串連接的方式:
- 使用join()函數(shù)將一組字符串合并為一個字符串。
- 使用+或+=符號將每個單獨的字符串添加到一個字符串中。
那么哪種方式更快呢?
現(xiàn)在,讓我們定義3個不同的函數(shù)來連接相同的字符串:
mylist = ["Yang", "Zhou", "is", "writing"]
# 使用'+'
def concat_plus():
result = ""
for word in mylist:
result += word + " "
return result
# 使用'join()'
def concat_join():
return " ".join(mylist)
# 直接連接而不使用列表
def concat_directly():
return "Yang" + "Zhou" + "is" + "writing"
根據(jù)你的第一印象,你認為哪個函數(shù)最快,哪個最慢?
真正的結(jié)果可能會讓你驚訝:
import timeit
print(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循環(huán)中逐個添加字符串更快。
原因很簡單。一方面,字符串在Python中是不可變的數(shù)據(jù),每次+=操作都會創(chuàng)建一個新字符串并復制舊字符串,這在計算上成本是昂貴的。
另一方面,.join()方法專門針對連接一系列字符串進行了優(yōu)化。它會預先計算出所生成字符串的大小,然后一次性創(chuàng)建它。因此,它避免了循環(huán)中+=操作帶來的開銷,從而使速度更快。
然而,在我們的測試中,速度最快的函數(shù)是直接連接字符串文字。它的高速度是由于:
- Python解釋器可以在編譯時優(yōu)化字符串文字的連接,將它們轉(zhuǎn)換為一個單獨的字符串文字。這里不涉及循環(huán)迭代或函數(shù)調(diào)用,因此是一種非常高效的操作。
- 由于所有字符串在編譯時都是已知的,Python可以非常快速地執(zhí)行此操作,比在循環(huán)中進行的運行時連接或經(jīng)過優(yōu)化的.join()方法要快得多。
總之,如果需要連接一組字符串,請選擇join()而不是+=。如果想要直接連接字符串,只需使用+即可。
2. 更快的列表創(chuàng)建:使用“[]”而不是“l(fā)ist()”
創(chuàng)建列表并不是很難的事情。常見的兩種方式是:
- 使用list()函數(shù)。
- 直接使用[]。
讓我們使用一個簡單的代碼片段來測試它們的性能:
import timeit
print(timeit.timeit('[]', number=10 ** 7))
# 0.1368238340364769
print(timeit.timeit(list, number=10 ** 7))
# 0.2958830420393497
結(jié)果顯示,執(zhí)行l(wèi)ist()函數(shù)比直接使用[]要慢。
這是因為[]是一種字面量語法,而list()是一個構(gòu)造函數(shù)調(diào)用。調(diào)用函數(shù)無疑需要額外的時間。
從同樣的邏輯出發(fā),在創(chuàng)建字典時,我們也應該使用{}而不是dict()。
3. 更快的成員測試:使用集合而不是列表
成員測試操作的性能在很大程度上依賴于底層數(shù)據(jù)結(jié)構(gòu):
import timeit
large_dataset = range(100000)
search_element = 2077
large_list = list(large_dataset)
large_set = set(large_dataset)
def list_membership_test():
return search_element in large_list
def set_membership_test():
return search_element in large_set
print(timeit.timeit(list_membership_test, number=1000))
# 0.01112208398990333
print(timeit.timeit(set_membership_test, number=1000))
# 3.27499583363533e-05
正如上述代碼所示,使用集合進行成員測試比使用列表更快。
為什么會這樣呢?
- 在Python的列表中,成員測試(列表中的元素)是通過迭代每個元素直到找到所需的元素或達到列表的末尾來完成。因此,此操作的時間復雜度為O(n)。
- Python中的集合實現(xiàn)形式為哈希表。在進行成員檢查(集合中的元素)時,Python使用哈希機制,其平均時間復雜度為O(1)。
這里的關(guān)鍵在于:在編寫程序時要仔細考慮底層數(shù)據(jù)結(jié)構(gòu)。正確利用合適的數(shù)據(jù)結(jié)構(gòu)可以顯著加快代碼的運行速度。
4. 更快的數(shù)據(jù)生成:使用推導式而不是for循環(huán)
Python中有四種推導式類型:列表推導式、字典推導式、集合推導式和生成器推導式。它們不僅為創(chuàng)建相對數(shù)據(jù)結(jié)構(gòu)提供了更簡潔的語法,而且比使用for循環(huán)更高效,因為它們在Python的C實現(xiàn)中進行了優(yōu)化。
import timeit
def generate_squares_for_loop():
squares = []
for i in range(1000):
squares.append(i * i)
return squares
def 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循環(huán)之間的簡單速度比較。結(jié)果顯示,列表推導式更快。
5. 更快的循環(huán):優(yōu)先使用局部變量
在Python中,訪問局部變量比訪問全局變量或?qū)ο蟮膶傩愿臁?/p>
以下是一個實例來證明這一點:
import timeit
class Example:
def __init__(self):
self.value = 0
obj = Example()
def test_dot_notation():
for _ in range(1000):
obj.value += 1
def test_local_variable():
value = obj.value
for _ in range(1000):
value += 1
obj.value = value
print(timeit.timeit(test_dot_notation, number=1000))
# 0.036605041939765215
print(timeit.timeit(test_local_variable, number=1000))
# 0.024470250005833805
這就是Python的工作原理。直觀地說,當一個函數(shù)被編譯時,其中的局部變量是已知的,但其他外部變量需要時間來檢索。
這可能是一個小問題,但是當處理大量數(shù)據(jù)時,我們可以利用它來優(yōu)化我們的代碼。
6. 更快的執(zhí)行速度:優(yōu)先使用內(nèi)置模塊和庫
當工程師們說到Python時,默認情況下指的是CPython。因為CPython是Python語言的默認實現(xiàn),也是使用最廣泛的實現(xiàn)。
鑒于它的大部分內(nèi)置模塊和庫都是用C語言編寫的,而C語言是一種更快且更底層的語言,因此我們應該利用這些內(nèi)置模塊和庫,避免重復勞動。
import timeit
import random
from collections import Counter
def count_frequency_custom(lst):
frequency = {}
for item in lst:
if item in frequency:
frequency[item] += 1
else:
frequency[item] = 1
return frequency
def 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
上面的程序比較了兩種統(tǒng)計列表中元素頻率的方法。可以看到,利用collections模塊中內(nèi)置的Counter函數(shù)比自己編寫的for循環(huán)更快、更簡潔、更好。
7. 更快的函數(shù)調(diào)用:利用緩存裝飾器輕松實現(xiàn)記憶化
緩存是一種常用的技術(shù),用于避免重復計算并加快程序的運行速度。
幸運的是,在大多數(shù)情況下,我們不需要自己編寫緩存處理代碼,因為Python為此提供了一個開箱即用的裝飾器來實現(xiàn)這個目的——@functools.cache。
例如,下面的代碼將執(zhí)行兩個生成斐波那契數(shù)的函數(shù),一個有緩存裝飾器,而另一個沒有:
import timeit
import functools
def fibonacci(n):
if n in (0, 1):
return n
return fibonacci(n - 1) + fibonacci(n - 2)
@functools.cache
def fibonacci_cached(n):
if n in (0, 1):
return n
return fibonacci_cached(n - 1) + fibonacci_cached(n - 2)
# 測試每個函數(shù)的執(zhí)行時間
print(timeit.timeit(lambda: fibonacci(30), number=1))
# 0.09499712497927248
print(timeit.timeit(lambda: fibonacci_cached(30), number=1))
# 6.458023563027382e-06
結(jié)果證明了@functools.cache裝飾器是如何使我們的代碼變得更快的。
基本的fibonacci函數(shù)效率較低,因為在計算fibonacci(30)結(jié)果的過程中,它會多次重新計算相同的斐波那契數(shù)。
而使用緩存的版本要快得多,因為它緩存了之前的計算結(jié)果。因此,它只計算每個斐波那契數(shù)一次,并且對于相同的參數(shù)再次調(diào)用時會從緩存中獲取結(jié)果。
僅僅添加一個內(nèi)置的裝飾器就可以帶來如此大的改進,這就是Pythonic的意義所在。??
8. 更快的無限循環(huán):優(yōu)先使用"while 1"而不是"while True"
要創(chuàng)建一個無限的while循環(huán),我們可以使用while True或while 1。
它們的性能差異通常可以忽略不計。但是,了解while 1稍微快一些還是很有趣的。
這源于1是一個字面常量,而True是Python全局作用域中需要查找的一個全局名稱,因此需要一點點額外開銷。
讓我們在代碼片段中進一步比較這兩種方式的真實性能:
import timeit
def loop_with_true():
i = 0
while True:
if i >= 1000:
break
i += 1
def loop_with_one():
i = 0
while 1:
if i >= 1000:
break
i += 1
print(timeit.timeit(loop_with_true, number=10000))
# 0.1733035419601947
print(timeit.timeit(loop_with_one, number=10000))
# 0.16412191605195403
正如所看到的,while 1的速度確實稍快一些。
然而,現(xiàn)代的Python解釋器(如CPython)已經(jīng)過高度優(yōu)化,這種差異通常是微不足道的。所以不需要擔心這種可忽略的差異。更不用說while True比while 1更易讀了。
9. 更快的啟動:巧妙地導入Python模塊
在Python腳本的頂部導入所有模塊似乎是很自然的。
實際上,我們并不需要這樣做。
而且,如果一個模塊太大,在需要時導入它是一個更好的選擇。
def my_function():
import heavy_module
# 函數(shù)的其余部分
以上代碼中,heavy_module是在函數(shù)內(nèi)部導入的。這是一種“延遲加載”的思想,即延遲到在調(diào)用my_function時才進行導入。
這種方法的好處是,如果在執(zhí)行腳本的過程中從未調(diào)用過my_function,那么heavy_module就不會被加載,從而節(jié)省資源并減少腳本的啟動時間。
結(jié)語
綜上所述,就是9個優(yōu)化Python代碼性能的實用技巧,但在實際應用時需要根據(jù)具體情況進行權(quán)衡和調(diào)整。通過綜合考慮代碼的性能、可讀性和可維護性,進而編寫出高效且易于理解的Python代碼。