Python“for”循环的更好方法

Posted

技术标签:

【中文标题】Python“for”循环的更好方法【英文标题】:A better way for a Python 'for' loop 【发布时间】:2017-10-29 02:30:56 【问题描述】:

我们都知道,在 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 快,因为两者都是生成器。是因为价值永远不会变吗?

【问题讨论】:

再试一试:for _ in itertools.repeat(None, count). 第二种方法的一个主要问题是它为整个丢弃列表分配存储空间。 但在实际代码中,循环体会更加复杂,并且支配着整个时序。如果迭代变量不重要,那么您只是在旋转。 @MaxPythone 你能举个例子吗?我很难相信 :) (虽然我不是专家 :)) @MaxPythone 谢谢你的回答,但我仍然很困惑。对于第一个示例,我认为主导执行的是获取/保存/检查您尝试获取的视频帧。或者,如果您可以直接从某个帧开始,那么运行一个空循环毫无意义。另一个例子也是如此;无论您使用什么算法进行基准测试,它都将取决于您在循环中所做的事情,而不是取决于您如何在 for 循环中增加 32 位计数器 【参考方案1】:

使用

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

是获得所有世界中最好的一种非显而易见的方式:微小的恒定空间需求,并且每次迭代都不会创建新对象。在幕后,repeat 的 C 代码使用原生 C 整数类型(不是 Python 整数对象!)来跟踪剩余计数。

因此,计数需要适合平台 C ssize_t 类型,通常在 32 位盒子上最多为 2**31 - 1,而在 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)

这对我的循环来说足够大了 ;-)

【讨论】:

再次感谢,如果我要搜索这些实现的源代码,我在哪里可以找到它们(这个和类似的标准库函数)? 这是一个相当长的学习曲线! itertools 的来源是github.com/python/cpython/blob/master/Modules/itertoolsmodule.c,repeat 的实现跨越了repeat_new 附近的几个不同的功能。我怎么知道这个?因为我已经使用 Python 的源代码 25 年了 ;-) @Paddy3118,真的是好坏参半。 repeat(None, 100)含义非常清楚:它传递None 100 次。循环不关心它传递的 哪个 值比通过例如range(100) 拼写更清楚。但因为它是一种新颖(很少见)的拼写,所以会影响易读性。不过,第二次看到它就很明显了,所以没什么大不了的。总体而言,仅就这一点而言,对我来说,它只比range(100) 略逊一筹。 Python 没有直接的方式说只是“执行此循环 N 次”,因此没有“真正的 Pythonic”方式。 另一方面,repeat() 并没有在幕后创建任何新的 Python int 对象,这是一个未记录的 CPython 实现细节,并且依赖于 that 以提高速度" 完全不像 Python 那样。【参考方案2】:

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

range(0, 1000) 的大小与list(range(0, 1000)) 的大小进行比较:Try It Online!。前者非常节省内存;不管大小,它只需要 48 个字节,而整个列表的大小是线性增加的。

第二种方法虽然更快,但占用了我在过去提到的那个记忆。 (另外,虽然0 占用 24 个字节,None 占用 16 个字节,但 10000 的数组大小相同。很有趣。可能是因为它们是指针)

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

第三个也不错,因为它不需要另一个堆栈值(而调用 range 需要调用堆栈上的另一个位置),但由于它慢了 6 倍,因此不值得。

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

【讨论】:

range 在 Python 3 中返回 range object,而不是生成器。证明这一点的一个具体品质是,您可以对其进行多次迭代,而生成器在迭代后会被消耗(因此为空)。【参考方案3】:

为了方便起见,这个答案提供了一个循环结构。有关使用 itertools.repeat 循环的更多背景信息,请查看 Tim Peters 的答案 above、Alex Martelli 的答案 here 和 Raymond Hettinger 的答案 here。

# 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 我不确定我是否理解你认为应该如何工作。当然你可以装饰一个特定的函数被调用1000次,但是你会失去调用那个函数的灵活性并且它只对那个被装饰的函数起作用。 我想调用函数do_something被调用10000次,装饰器可能会将do_something更改为for _ in loop(10000): do_something() @Tweakimp 如果您总是想调用它 10k 次,那当然是有道理的,并且为此使用装饰器并不少见。但这是一个非常具体的用例,您可以在这里扩展基本思想,即找到一个“更好”的 for 循环。【参考方案4】:

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

Range 是一个慢速函数,我只在需要运行不需要速度的小代码时才使用它,例如range(0,50)。我认为您无法比较这三种方法;它们完全不同。

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

【讨论】:

不正确。在 Python 3 中,range 生成一个迭代器。它相当于 Python 2 的xrange。只有第二种方法存在内存问题。 @TomKarzes 仍然不正确(尽管更正确)。它产生一个range object。范围对象不是迭代器或生成器;它可以被迭代多次而不被消耗。

以上是关于Python“for”循环的更好方法的主要内容,如果未能解决你的问题,请参考以下文章

[python篇] [伯乐在线][1]永远别写for循环

python循环之for循环与基本的数据类型以及内置方法

python中for循环的用法

Python - 更好的循环解决方案 - 出现错误后重新运行并在 3 次尝试后忽略该错误

python系列教程150——for循环

python中for循环的用法