2017-10-29 165 views
58

我們都知道在Python中執行一段語句的常用方法是使用for循環。Python for'循環的更好方法

這樣做的一般方法是,

# I am assuming iterated list is redundant. 
# Just the number of execution matters. 
for _ in range(count): 
    pass 

我相信沒有人會認爲上面的代碼是通用的實現,但還有另一種選擇。通過乘以引用來創建Python列表創建的速度。

# Uncommon way. 
for _ in [0] * count: 
    pass 

還有舊的while的方式。

i = 0 
while i < count: 
    i += 1 

我測試了這些方法的執行時間。這是代碼。

import timeit 

repeat = 10 
total = 10 

setup = """ 
count = 100000 
""" 

test1 = """ 
for _ in range(count): 
    pass 
""" 

test2 = """ 
for _ in [0] * count: 
    pass 
""" 

test3 = """ 
i = 0 
while i < count: 
    i += 1 
""" 

print(min(timeit.Timer(test1, setup=setup).repeat(repeat, total))) 
print(min(timeit.Timer(test2, setup=setup).repeat(repeat, total))) 
print(min(timeit.Timer(test3, setup=setup).repeat(repeat, total))) 

# Results 
0.02238852552017738 
0.011760978361696095 
0.06971727824807639 

我不會發起主題,如果有一個小的差異,但可以看出,速度的差異是100%。爲什麼Python不鼓勵這種用法,如果第二種方法更有效率?有沒有更好的辦法?

測試使用Windows 10Python 3.6完成。

繼@Tim彼得斯的建議,

. 
. 
. 
test4 = """ 
for _ in itertools.repeat(None, count): 
    pass 
""" 
print(min(timeit.Timer(test1, setup=setup).repeat(repeat, total))) 
print(min(timeit.Timer(test2, setup=setup).repeat(repeat, total))) 
print(min(timeit.Timer(test3, setup=setup).repeat(repeat, total))) 
print(min(timeit.Timer(test4, setup=setup).repeat(repeat, total))) 

# Gives 
0.02306803115612352 
0.013021619340942758 
0.06400113461638746 
0.008105080015739174 

它提供了一個更好的辦法,而這幾乎回答我的問題。

爲什麼這比range快,因爲它們都是發電機。是否因爲價值從不改變?

+8

還有一次嘗試:'for _ in itertools.repeat(None,count)'。 –

+8

第二種方法的一個主要問題是它爲整個丟棄列表分配存儲空間。 –

+9

但是在實際的代碼中,循環的主體將更加複雜,並且在所有時間中占主導地位。如果迭代變量不重要,則只需旋轉車輪即可。 – hpaulj

回答

77

使用

for _ in itertools.repeat(None, count) 
    do something 

是獲得了世界上最好的非顯而易見的方式:微小不變的空間要求,並且每次迭代創建新的對象。在封面下,repeat的C代碼使用本地C整數類型(不是Python整數對象!)來跟蹤剩餘的計數。

出於這個原因,計數需要適合在平臺的C ssize_t類型,其通常至多2**31 - 1是在32位中,並在這裏對64位的框:

>>> itertools.repeat(None, 2**63) 
Traceback (most recent call last): 
    ... 
OverflowError: Python int too large to convert to C ssize_t 

>>> itertools.repeat(None, 2**63-1) 
repeat(None, 9223372036854775807) 

哪對我的循環來說是很大的;-)

+0

再次感謝,如果我要搜索這些實現的源代碼,我可以在哪裏找到它們(這個和類似的標準庫函數)? –

+2

這真是一條學習曲線! itertools的源代碼位於https://github.com/python/cpython/blob/master/Modules/itertoolsmodule.c,'repeat'的實現跨越'repeat_new'幾個不同的函數。我怎麼知道這個?因爲我已經玩了25年的Python源代碼;-) –

+1

嗯,我已經知道你參加了Python項目,所以我想盡可能多的提取信息,而你在這裏:)你的幫助表示讚賞。 –

0

前兩種方法需要爲每次迭代分配內存塊,而第三種方法只需爲每次迭代進行一步。

範圍是一個緩慢的功能,我只有在需要運行不需要速度的小代碼時才使用它,例如range(0,50)。我認爲你不能比較這三種方法;他們完全不同。

根據以下評論,第一種情況只對Python 2.7有效,在Python 3中它像xrange一樣工作,並且不爲每次迭代分配塊。我測試了一下,他是對的。

+6

錯誤。在Python 3中,'range'產生一個迭代器。它相當於Python 2的'xrange'。只有第二種方法存在內存問題。 –

+0

@TomKarzes仍然不正確(雖然更正確)。它產生一個['range'對象](https://docs.python.org/3/library/stdtypes.html#typesseq-range)。範圍對象不是迭代器或生成器;它可以迭代多次而不被消耗。 – jpmc26

11

第一種方法(在Python 3中)創建一個範圍對象,它可以遍歷值的範圍。 (它就像一個生成器對象,但可以遍歷它幾次)。它不佔用太多的內存,因爲它不包含整個範圍的值,只是當前值和最大值,它隨着步長(默認1),直到它達到或超過最大值。

range(0, 1000)的尺寸與list(range(0, 1000))的尺寸進行比較:Try It Online!。前者非常有記憶效率;無論大小如何,只需要48個字節,而整個列表在大小上線性增加。

第二種方法雖然速度更快,但佔用了我過去所談論的內存。 (另外,雖然0佔用24個字節,而None佔16個,但10000的數組具有相同的大小。有趣。可能是因爲它們是指針)

有意思的是,[0] * 10000小於list(range(10000))大約10000,這種方式是有道理的,因爲在第一個中,所有東西都是相同的原始值,所以它可以被優化。

第三個也不錯,因爲它不需要另一個堆棧值(而調用range需要調用堆棧上的另一個位置),但由於速度慢6倍,所以不值得。

最後一個可能是最快的,因爲itertools很酷:P我認爲它使用了一些C庫優化,如果我沒有記錯的話。

+0

'range'在Python 3中返回['range'對象](https://docs.python.org/3/library/stdtypes.html#typesseq-range),而不是生成器。證明這一點的一個特定質量是可以多次遍歷它,而生成器一旦迭代就消耗(因此爲空)。 – jpmc26

+0

@ jpmc26啊,是的,謝謝你糾正我:) – HyperNeutrino