Python提速秘籍:九個(gè)讓你的代碼飛速運(yùn)行的巧妙技巧!
引言
“Python太慢了?!边@種觀點(diǎn)在編程語(yǔ)言的討論中頻頻出現(xiàn),常常使人忽視Python的眾多優(yōu)點(diǎn)。
但事實(shí)真的如此嗎?與普遍看法相反,如果你掌握了Python式的編程技巧,Python其實(shí)可以像冠軍選手一樣快速奔跑。
在表面之下,精通Python的開(kāi)發(fā)者們掌握著一系列微妙而強(qiáng)大的技巧,這些技巧能顯著提升他們代碼的性能,遠(yuǎn)超常規(guī)水平。這些不僅僅是技巧,它們甚至改變了游戲規(guī)則。
今天,我們將揭示九種變革性的策略,這些策略可以徹底改變你對(duì)Python編程的看法。這些策略乍看之下或許很簡(jiǎn)單,但它們具有強(qiáng)大的效力,能以你從未想象的方式提升效率。準(zhǔn)備好給你的Python技能加速了嗎?讓我們深入了解并開(kāi)始優(yōu)化吧!
1.join 或 +:更快的字符串連接
如果你的程序中經(jīng)常進(jìn)行字符串操作,那么字符串連接可能會(huì)成為你的 Python 程序的瓶頸。
基本上,在 Python 中有兩種字符串連接的方法:
- 使用join()函數(shù)將一系列字符串合并為一個(gè)
- 使用+或+=符號(hào)逐一將每個(gè)字符串添加到一個(gè)中
那么哪種方法更快?廢話少說(shuō),下面我們使用3種不同的方式連接相同的字符串:
str_list = ['Facts', 'speak', 'louder', 'than', 'words!']
# 使用 + 號(hào)
def concat_plus(strings):
result = ''
for word in strings:
result += word + ' '
return result
# 使用 join() 方法
def concat_join(strings):
return ' '.join(strings)
# 直接連接
def concat_directly():
return 'Facts' + 'speak' + 'louder' + 'than' + 'words!'
根據(jù)您那作為男士or女士神奇的第六感(????),悄悄告訴我您認(rèn)為哪個(gè)函數(shù)速度最快,哪個(gè)最慢?實(shí)際結(jié)果可能會(huì)讓您感到驚訝哦??????:
import timeit
print(f'The plus symbol: {timeit.timeit(concat_plus, number=10000)}')
print(f'The join function: {timeit.timeit(concat_join, number=10000)}')
print(f'The direct concatenation: {timeit.timeit(concat_directly, number=10000)}')
圖片
如上所示,對(duì)于字符串連接,join() 方法比通過(guò)循環(huán)逐個(gè)添加字符串要快。
原因很簡(jiǎn)單。一方面,在Python中,字符串是不可變數(shù)據(jù),每個(gè) += 操作都會(huì)伴隨新字符串變量的創(chuàng)建和舊字符串的復(fù)制,這會(huì)額外消耗更多的計(jì)算資源。另一方面,.join() 方法專門針對(duì)連接列表字符串進(jìn)行了優(yōu)化。它預(yù)先計(jì)算生成字符串的大小,然后一次性為其分配存儲(chǔ)空間。因此,它避免了循環(huán)中的 += 操作帶來(lái)的開(kāi)銷,因此更快。
然而,在我們的測(cè)試中,最快的函數(shù)是直接連接字符串字面量。其高速度歸結(jié)于:
- Python 解釋器可以在編譯時(shí)優(yōu)化字符串字面值的連接,將它們轉(zhuǎn)換為單個(gè)字符串字面值。這里沒(méi)有涉及到循環(huán)迭代或函數(shù)調(diào)用,因此操作效率非常高。
- 由于在編譯時(shí)已知所有字符串,Python 可以非??焖俚貓?zhí)行此操作,比在循環(huán)中運(yùn)行時(shí)連接甚至優(yōu)化過(guò)的 .join() 方法都要快得多。
總之,如果您需要連接字符串列表,請(qǐng)選擇 join() 而不是 +=。如果您想直接連接字符串,只需使用 + 即可。
2.更快的列表創(chuàng)建:選擇“[]”而非“l(fā)ist()”
創(chuàng)建列表并不困難。兩種常見(jiàn)的方法是:
- 使用 list() 函數(shù)
- 直接使用 []:
import timeit
print('The List Creation:')
print(f"[]: {timeit.timeit('[]', number=10 ** 7)}")
print(f'The list function: {timeit.timeit(list, number=10 ** 7)}')
圖片
正如結(jié)果所示,直接使用 [] 比執(zhí)行 list() 函數(shù)要快差不多2倍。這是因?yàn)?nbsp;[] 是一種字面語(yǔ)法,而 list() 是一個(gè)構(gòu)造函數(shù)調(diào)用。毫無(wú)疑問(wèn),調(diào)用函數(shù)需要額外的時(shí)間。相同的邏輯,在創(chuàng)建字典時(shí),我們也應(yīng)該利用 {} 而不是 dict()。
3.更快的成員檢查:用 Set 而不用 List
成員檢查操作的性能在很大程度上取決于底層數(shù)據(jù)結(jié)構(gòu),一起來(lái)看看下面這個(gè)例子:
import timeit
target_dataset = range(1000000)
search_element = 1314
target_list = list(target_dataset)
target_set = set(target_dataset)
def list_membership_test():
return search_element in target_list
def set_membership_test():
return search_element in target_set
print(f'The list membership test: {timeit.timeit(list_membership_test, number=1000)}')
print(f'The set membership test: {timeit.timeit(set_membership_test, number=1000)}')
圖片
結(jié)果顯示,在集合中進(jìn)行成員檢查比在列表中快得多。我還發(fā)現(xiàn)一個(gè)問(wèn)題,那就是搜索的元素越靠前則耗時(shí)越短,如果搜索一個(gè)不存在的元素則耗時(shí)最長(zhǎng)。上面我們搜索的目標(biāo)元素是1314,如果我們搜索一個(gè)不存在的元素1314520,則明顯耗時(shí)更多:
圖片
因?yàn)樗阉饕粋€(gè)不存在的元素必須遍歷完整個(gè)列表或集合。By the way,從這個(gè)例子可以看出要做到一生一世(1314)很容易,因?yàn)槊總€(gè)人生來(lái)便有,但是要做到一生一世我愛(ài)你(1314520)卻并不簡(jiǎn)單,因?yàn)樾枰冻龈嗟拇鷥r(jià)。哈哈??????,開(kāi)個(gè)玩笑,扯遠(yuǎn)了,權(quán)當(dāng)是給您枯燥的閱讀帶來(lái)一點(diǎn)小樂(lè)趣!
回到主題,為什么成員檢查用集合比列表更快呢?
- 在Python列表中,成員檢查(element in list)是通過(guò)迭代每個(gè)元素直到找到所需元素或達(dá)到列表末尾來(lái)執(zhí)行的。因此,這個(gè)操作的時(shí)間復(fù)雜度為O(n)。
- 在Python中,集合用哈希表實(shí)現(xiàn)。在檢查成員關(guān)系(element in set)時(shí),Python使用哈希機(jī)制,其時(shí)間復(fù)雜度平均為O(1)。
這里的要點(diǎn)是在編寫(xiě)程序時(shí)仔細(xì)考慮底層數(shù)據(jù)結(jié)構(gòu)。利用正確的數(shù)據(jù)結(jié)構(gòu)可以顯著加快我們的代碼速度。
4.更快的數(shù)據(jù)生成:用推導(dǎo)式而不用 for 循環(huán)
Python 中有四種推導(dǎo)式:列表、字典、集合和生成器。它們不僅提供更簡(jiǎn)潔的語(yǔ)法來(lái)創(chuàng)建相關(guān)的數(shù)據(jù)結(jié)構(gòu),而且比使用 for 循環(huán)的性能更好。因?yàn)樗鼈兪褂?C 語(yǔ)言實(shí)現(xiàn)的,性能進(jìn)行了優(yōu)化。
一起看看下面這個(gè)生成1-10000的立方示例:
import timeit
def generate_cubes_for_loop():
cubes = []
for i in range(10000):
cubes.append(i * i * i)
return cubes
def generate_cubes_comprehension():
return [i * i * i for i in range(10000)]
print(f'For loop: {timeit.timeit(generate_cubes_for_loop, number=10000)}')
print(f'Comprehension: {timeit.timeit(generate_cubes_comprehension, number=10000)}')
上述代碼只是列表推導(dǎo)式和 for 循環(huán)之間的簡(jiǎn)單速度比較。正如如結(jié)果所示,列表推導(dǎo)式更快。
5.更快的循環(huán):優(yōu)先用局部變量
在Python中,訪問(wèn)局部變量比訪問(wèn)全局變量或?qū)ο髮傩砸?。這里用一個(gè)簡(jiǎn)單例子來(lái)證明這一點(diǎn):
import timeit
class Test:
def __init__(self):
self.value = 0
obj = Test()
def access_global_variable():
for _ in range(1000):
obj.value += 1
def access_local_variable():
value = obj.value
for _ in range(1000):
value += 1
print(f'Access global variable: {timeit.timeit(access_global_variable, number=1000)}')
print(f'Access local variable: {timeit.timeit(access_local_variable, number=1000)}')
這就是 Python 的工作原理。直觀地說(shuō),當(dāng)函數(shù)編譯時(shí),其中的局部變量是已知的,但其他外部變量則需要時(shí)間來(lái)檢索。
這只是一個(gè)很小的改良,但有時(shí)候我們?nèi)笨梢岳盟鼇?lái)優(yōu)化我們的代碼,特別是在處理大數(shù)據(jù)集時(shí)。
6.更快的執(zhí)行:優(yōu)先使用內(nèi)置模塊和庫(kù)
當(dāng)工程師們說(shuō) Python 時(shí),默認(rèn)是指 CPython。因?yàn)?CPython 是 Python 語(yǔ)言的默認(rèn)和最廣泛使用的實(shí)現(xiàn)。
考慮到大多數(shù)內(nèi)置模塊和庫(kù)都是用更快速和更底層的語(yǔ)言 C 編寫(xiě)的,我們應(yīng)該盡可能利用這些內(nèi)置工具并避免重復(fù)發(fā)明輪子。
import timeit
import random
from collections import Counter
def counter_custom(lst):
frequency = {}
for item in lst:
if item in frequency:
frequency[item] += 1
else:
frequency[item] = 1
return frequency
def counter_builtin(lst):
return Counter(lst)
target_dataset = [random.randint(0, 100) for _ in range(1000)]
print(f'Counter custom: {timeit.timeit(lambda: counter_custom(target_dataset), number=100)}')
print(f'Counter builtin: {timeit.timeit(lambda: counter_custom(target_dataset), number=100)}')
這里比較了在列表中計(jì)算元素頻率的兩種方法。正如我們所看到的,利用 collections 模塊中的內(nèi)置 Counter 比自己編寫(xiě) for 循環(huán)更快,更整潔,更好(但有時(shí)候自定義的性能又會(huì)比內(nèi)置模塊更好,尚不知道原因)。
7.更快的函數(shù)調(diào)用:利用緩存裝飾器
緩存是一種常用的技術(shù),用于避免重復(fù)計(jì)算并加快程序的運(yùn)行速度。幸運(yùn)的是,在大多數(shù)情況下,我們不需要編寫(xiě)自己的緩存處理代碼,因?yàn)镻ython提供了一個(gè)用于此目的的開(kāi)箱即用的裝飾器 — @functools.cache。
例如,以下代碼將執(zhí)行兩個(gè)斐波那契數(shù)生成函數(shù),一個(gè)帶有緩存裝飾器,而另一個(gè)沒(méi)有:
import timeit
from functools import cache
def fibonacci_norm(n):
if n <= 1:
return n
return fibonacci_norm(n - 1) + fibonacci_norm(n - 2)
@cache
def fibonacci_cached(n):
if n <= 1:
return n
return fibonacci_cached(n - 1) + fibonacci_cached(n - 2)
print(f'fibonacci normal: {timeit.timeit(lambda: fibonacci_norm(30), number=1)}')
print(f'fibonacci cached: {timeit.timeit(lambda: fibonacci_cached(30), number=1)}')
結(jié)果顯示 cache 裝飾器版本比普通版本的速度快得多。
普通的斐波那契函數(shù)效率低下,因?yàn)樵讷@取 fibonacci(30) 的結(jié)果過(guò)程中,它多次重新計(jì)算相同的斐波那契數(shù)。
緩存版本明顯更快,因?yàn)樗彺媪讼惹坝?jì)算的結(jié)果。因此,每個(gè)斐波那契數(shù)它只計(jì)算一次,并且使用相同參數(shù)進(jìn)行的后續(xù)調(diào)用都從緩存中獲取。
僅僅添加一個(gè)內(nèi)置裝飾器就可以帶來(lái)如此大的性能提升,這就是 Pythonic 的意義所在。??
8.更快的無(wú)限循環(huán): 優(yōu)先選擇“while 1”而不是“while True”
要?jiǎng)?chuàng)建一個(gè)無(wú)限循環(huán),我們可以使用 while True 或 while 1 。它們的性能差異通常是可以忽略的。但有趣的是 while 1 稍微更快。這源于 1 是字面值,而 True 是 Python 全局范圍內(nèi)需要查找的全局名稱,因此需要微小的額外開(kāi)銷。
我們也通過(guò)一個(gè)簡(jiǎn)單的示例比較這兩種方式的性能:
import timeit
def infinite_loop_with_true():
result = 0
while True:
if result >= 10000:
break
result += 1
def infinite_loop_with_one():
result = 0
while 1:
if result >= 10000:
break
result += 1
print(f'Infinite loop with true: {timeit.timeit(infinite_loop_with_true, number=10000)}')
print(f'Infinite loop with one: {timeit.timeit(infinite_loop_with_one, number=10000)}')
正如我們所看到的,while 1 確實(shí)略快。但是,現(xiàn)代 Python 解釋器(如CPython)經(jīng)過(guò)高度優(yōu)化,這樣的差異通常微不足道。因此,我們無(wú)需在意這種微不足道的差異。另外,從代碼可讀性角度來(lái)說(shuō),其實(shí) while True 的可讀性比 while 1 更強(qiáng)。
9.更快的腳本啟動(dòng):智能導(dǎo)入Python模塊
通常情況下,我們都習(xí)慣在Python 腳本頂部導(dǎo)入所有模塊。事實(shí)上,有些時(shí)候不必這樣做。此外,如果模塊太大,則按需導(dǎo)入可能會(huì)是一個(gè)更好的主意。比如,在用到模塊的函數(shù)內(nèi)部導(dǎo)入:
def target_function():
import specific_module
# rest of the function
如上面的代碼所示,specific_module 在函數(shù)內(nèi)部執(zhí)行導(dǎo)入操作。這是“惰性加載”的思想,在函數(shù)調(diào)用時(shí)才導(dǎo)入指定模塊。
這種方法的好處是,如果在腳本執(zhí)行期間從未調(diào)用 target_function,則永遠(yuǎn)不會(huì)加載 specific_module,從而節(jié)省資源并減少腳本的啟動(dòng)時(shí)間。