Python“ for”循环的更好方法


66

我们都知道,在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 Peters的建议,

.
.
.
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

3
@MaxPythone您能举个例子吗?我很难相信:)(不是我虽然是专家:))
蚂蚁

3
@MaxPythone谢谢您的回答,但我仍然感到困惑。对于第一个示例,我认为主导执行的是获取/保存/检查您尝试获取的视频帧。或者,如果您可以直接从某个特定的帧开始,则没有必要运行空循环。同样在另一个例子中;无论您使用哪种算法进行基准测试,它都将由您在循环中执行的操作决定,而不是由您如何增加for循环中的32位计数器来决定
Ant

Answers:


93

使用

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

获得最佳状态的一种显而易见的方法是:微小的恒定空间需求,并且每次迭代都不会创建新的对象。在幕后,C代码repeat使用本机C整数类型(不是Python整数对象!)来跟踪剩余计数。

因此,该计数需要适合平台Cssize_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)

这对于我的循环来说很大;-)


再次感谢,如果我要搜索这些实现的源代码,在哪里可以找到它们(此库和类似的标准库函数)?
Max Paython

4
那是一条学习曲线!itertools的源代码位于github.com/python/cpython/blob/master/Modules/itertoolsmodule.c,并且span的实现repeat跨越了附近的几个不同功能repeat_new。我怎么知道 因为我使用Python的源代码已有25年了;-)
Tim Peters

1
@ Paddy3118,真的是个混蛋。该意义repeat(None, 100)是相当清楚的:它提供了None100倍。该循环不关心它提供值(s)为更清楚一点这种方式比通过,例如range(100)拼写。但是因为这是一种新颖的(很少见的)拼写,所以会损害易读性。但是,您第二次看到它很明显,所以没什么大不了的。总体而言,仅此一项,对我而言,它的Pythonic仅仅比少一点range(100)。Python没有直接的方法可以说只是“执行此循环N次”,因此没有“真正的Python式”方法可以做到。
蒂姆·彼得斯

1
另一方面,在repeat()幕后不会创建任何新的Python int对象是一个未记录的CPython实现细节,并且依靠“追求速度”就变得与Python无关。
蒂姆·彼得斯

11

第一个方法(在Python 3中)创建一个range对象,该对象可以遍历值的范围。(这就像一个生成器对象,但是您可以对其进行多次迭代。)它不占用太多内存,因为它不包含整个值范围,而仅包含当前值和最大值,并且该值一直以步长(默认为1),直到达到或超过最大值。

将的大小range(0, 1000)与的大小进行比较list(range(0, 1000))在线尝试!。前者的内存效率很高;无论大小如何,它仅占用48个字节,而整个列表的大小则线性增加。

第二种方法虽然更快,但是却占用了我在过去所讨论的那种内存。(此外,似乎虽然0占用24个字节并None占用16个字节,但10000每个数组的大小都相同。有趣。可能是因为它们是指针)

有趣的是,[0] * 10000它小于list(range(10000))约10000,这是有道理的,因为在第一个中,所有东西都是相同的原始值,因此可以对其进行优化。

第三个也是不错的,因为它不需要另一个堆栈值(而调用则range需要在调用堆栈上另一个位置),尽管由于它慢了6倍,所以不值得。

最后一个可能是最快的,只是因为itertools这样很酷:如果我没记错的话,PI认为它使用了一些C库优化。


range返回Python 3中的range对象,而不是生成器。证明这一点的一种特殊品质是,您可以对其进行多次迭代,而一旦迭代,生成器就会被消耗(因此是空的)。
jpmc26

1

为方便起见,此答案提供了一个循环构造。有关使用循环的其他背景itertools.repeat的查找蒂姆·彼得斯的回答以上,亚历克斯·马尔泰利的答案在这里和雷蒙德赫廷杰的答案在这里

# loop.py

"""
Faster for-looping in CPython for cases where intermediate integers
from `range(x)` are not needed.

Example Usage:
--------------

from loop import loop

for _ in loop(10000):
    do_something()

# or:

results = [calc_value() for _ in loop(10000)]
"""

from itertools import repeat
from functools import partial

loop = partial(repeat, None)

将循环用作装饰器会更好吗,您可以这样做:@loop(10000) do_something()
Tweakimp

@Tweakimp我不确定我是否理解您的想法。当然,您可以将一个特定的函数修饰为1000次,但是您在调用该函数时会失去灵活性,并且仅适用于修饰的函数。
Darkonaut

我想调用该函数do_something以调用10000次,并且装饰器可能会更改do_somethingfor _ in loop(10000): do_something()
Tweakimp

@Tweakimp如果您总是要调用它10k次,那肯定是有道理的,为此使用装饰器并不罕见。但这是一个非常特定的用例,然后在这里扩展基本概念,即找到一个“更好的” for循环。
Darkonaut

0

前两种方法需要为每次迭代分配内存块,而第三种方法只是为每次迭代分配一个步骤。

范围是一个缓慢的函数,仅当我必须运行不需要速度的小代码时才使用它range(0,50)。我认为您无法比较这三种方法;他们是完全不同的。

根据下面的评论,第一种情况仅对Python 2.7有效,在Python 3中,它的工作方式类似于xrange,并且不会为每次迭代分配一个块。我测试了一下,他是对的。


6
不正确 在Python 3中,range产生一个迭代器。它等效于Python 2的xrange。只有第二种方法有内存问题。
汤姆·卡兹斯

@TomKarzes仍然不正确(尽管更正确)。它产生一个range对象。范围对象不是迭代器或生成器;它不是迭代器。它可以多次迭代而不会被消耗。
jpmc26 '11
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.