2017-10-29 166 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