[英]Using a for loop vs list comprehension to optimise code
為什么與使用for循環而不是使用列表理解的most_all_b相比,most_all_a效率更高? 他們肯定都是O(n)嗎? 編輯:在另一個問題中的答案是非特定的,基本上說一個取決於情況,可以比另一個更快。 那么,這種情況呢?
def almost_all_a(numbers):
sumall = sum(numbers)
rangelen = range(len(numbers))
return [sumall - numbers[i] for i in rangelen]
def almost_all_b(numbers):
sumall = sum(numbers)
for i in range(len(numbers)):
numbers[i] = sumall - numbers[i]
有關問題的答案包含了所有內容,但以某種方式被隱藏了。
讓我們看一看almost_all_a
是什么:它建立一個與原始列表大小相同的新列表,然后返回該新列表。 對於大列表,這將使用列表所需內存的兩倍(假設此處為數字列表)。 並以這種方式調用該函數: nums = almost_all_a(nums)
,您只是在構建一個新列表,完成nums = almost_all_a(nums)
丟棄前一個列表。 對性能的兩個影響:需要(臨時)內存,並且需要垃圾收集器清理舊列表。
在almost_all_b
中,什么都沒有發生:您只是在更改列表元素:沒有額外的分配(內存增益),也沒有任何東西可以收集(執行時間增益)。
TL / DR:是什么讓a
版本丟失,重要的是它歸結為分配一個新的列表,而相關的回答說:
由於創建和擴展列表的開銷很大,因此使用列表推導代替不構建列表的循環,無意義地累積無意義的值列表然后將其丟棄的方法通常會較慢。
您的復雜性分析是正確的: n
操作來計算總和加上n
操作來計算在這兩種情況下的列表使得O(n)
但是,在我們談論速度之前,您肯定已經注意到, almost_all_b
具有副作用,而almost_all_a
沒有副作用。 更糟糕的是, almost_all_b
不是冪等的。 如果您重復調用almost_all_b
,則每次都會修改參數numbers
。 除非您有很好的理由,否則您應該更喜歡almost_all_a
而almost_all_b
因為它更易於理解並且不易出錯 。
我將嘗試用timeit
確認您的斷言( almost_all_a
是almost_all_a
[比] almost_all_b
有效 ):
>>> from timeit import timeit
>>> ns=list(range(100))
>>> timeit(lambda: almost_all_a(ns), number=10000)
0.06381335399782984
>>> timeit(lambda: almost_all_b(ns), number=10000)
2.3228586789991823
哇! almost_all_a
比快約35倍almost_all_b
! 不,那是個玩笑。 您可以看到發生了什么: almost_all_b
[1,...,90]
進行了almost_all_b
10000次的almost_all_b
,所以該數字瘋狂地增加了:
>>> len(str(ns[0])) # number of digits of the first element!
19959
好的,那只是說服您避免具有副作用的功能。
現在,真正的測試:
>>> timeit('ns=list(range(100));almost_all(ns)', globals={'almost_all':almost_all_a})
5.720672591000039
>>> timeit('ns=list(range(100));almost_all(ns)', globals={'almost_all':almost_all_b})
5.937547881
請注意,基准測試可能會在另一個列表或另一個平台上給出不同的結果。 (考慮一下,如果列表占用了90%的可用RAM,將會發生什么。)但是讓我們假設我們可以一概而論。
讓我們看一下字節碼:
>>> import dis
>>> dis.dis(almost_all_a)
2 0 LOAD_GLOBAL 0 (sum)
2 LOAD_DEREF 0 (numbers)
4 CALL_FUNCTION 1
6 STORE_DEREF 1 (sumall)
3 8 LOAD_GLOBAL 1 (range)
10 LOAD_GLOBAL 2 (len)
12 LOAD_DEREF 0 (numbers)
14 CALL_FUNCTION 1
16 CALL_FUNCTION 1
18 STORE_FAST 1 (rangelen)
4 20 LOAD_CLOSURE 0 (numbers)
22 LOAD_CLOSURE 1 (sumall)
24 BUILD_TUPLE 2
26 LOAD_CONST 1 (<code object <listcomp> at 0x7fdc551dee40, file "<stdin>", line 4>)
28 LOAD_CONST 2 ('almost_all_a.<locals>.<listcomp>')
30 MAKE_FUNCTION 8
32 LOAD_FAST 1 (rangelen)
34 GET_ITER
36 CALL_FUNCTION 1
38 RETURN_VALUE
和:
>>> dis.dis(almost_all_b)
2 0 LOAD_GLOBAL 0 (sum)
2 LOAD_FAST 0 (numbers)
4 CALL_FUNCTION 1
6 STORE_FAST 1 (sumall)
3 8 SETUP_LOOP 36 (to 46)
10 LOAD_GLOBAL 1 (range)
12 LOAD_GLOBAL 2 (len)
14 LOAD_FAST 0 (numbers)
16 CALL_FUNCTION 1
18 CALL_FUNCTION 1
20 GET_ITER
>> 22 FOR_ITER 20 (to 44)
24 STORE_FAST 2 (i)
4 26 LOAD_FAST 1 (sumall)
28 LOAD_FAST 0 (numbers)
30 LOAD_FAST 2 (i)
32 BINARY_SUBSCR
34 BINARY_SUBTRACT
36 LOAD_FAST 0 (numbers)
38 LOAD_FAST 2 (i)
40 STORE_SUBSCR
42 JUMP_ABSOLUTE 22
>> 44 POP_BLOCK
>> 46 LOAD_CONST 0 (None)
48 RETURN_VALUE
開始幾乎是一樣的。 然后,您將獲得一個類似於黑匣子的列表理解功能。 如果打開盒子,我們將看到:
>>> dis.dis(almost_all_a.__code__.co_consts[1])
4 0 BUILD_LIST 0
2 LOAD_FAST 0 (.0)
>> 4 FOR_ITER 16 (to 22)
6 STORE_FAST 1 (i)
8 LOAD_DEREF 1 (sumall)
10 LOAD_DEREF 0 (numbers)
12 LOAD_FAST 1 (i)
14 BINARY_SUBSCR
16 BINARY_SUBTRACT
18 LIST_APPEND 2
20 JUMP_ABSOLUTE 4
>> 22 RETURN_VALUE
您有兩個區別:
sumall
和numbers
都加載了LOAD_DEREF
而不是LOAD_FAST
(這對於閉包是正常的),並且應該慢一些; LIST_APPEND
替換對numbers[i]
LOAD_FAST(numbers)/LOAD_FAST(i)/STORE_SUBSCR
第36-40行的LOAD_FAST(numbers)/LOAD_FAST(i)/STORE_SUBSCR
)。 我的猜測是開銷來自該分配。
您可以重寫almost_all_a
,使其更整潔,因為您不需要索引:
def almost_all_c(numbers):
sumall = sum(numbers)
return [sumall - n for n in numbers]
這個版本(在我的示例+平台上)比almost_all_a
: almost_all_a
:
>>> timeit('ns=list(range(100));almost_all(ns)', globals={'almost_all':almost_all_a})
5.755438814000172
>>> timeit('ns=list(range(100));almost_all(ns)', globals={'almost_all':almost_all_b})
5.93645353099987
>>> timeit('ns=list(range(100));almost_all(ns)', globals={'almost_all':almost_all_c})
4.571863283000084
(請注意,在Python中,更整潔的版本是最快的版本。) almost_all_a
和almost_all_c
之間的區別是使用對第i
個numbers
項的訪問(您可以反編譯要檢查的almost_all_c
)。
我似乎這里的瓶頸是訪問第i
個numbers
:
almost_all_a
; almost_all_b
中almost_all_b
; almost_all_c
這就是為什么almost_all_a
快於almost_all_b
的原因。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.