为什么使用“for _ in range(n)”比“for _ in [""]*n”慢?

6

在测试替代for _ in range(n)(用于执行某些操作n次,即使该操作不依赖于n的值)时,我注意到另一种模式的表述更快,即for _ in [""] * n

例如:

timeit('for _ in range(10^1000): pass', number=1000000)

返回16.4秒;

然而,

timeit('for _ in [""]*(10^1000): pass', number=1000000)

需要10.7秒。

为什么在Python 3中,[""] * 10^1000range(10^1000)快那么多?

所有的测试都是使用Python 3.3进行的。


5
顺便提一下,当你在迭代时并不关心数值本身时,使用下划线是惯用法:for _ in range(n) - Kevin
5
就这个问题而言,timeit('for x in range(10): pass', number=10000000) 的结果为 5.320733592294609,而 timeit('for x in [0]*10: pass',number=10000000) 的结果为 4.120525842738559 - kylieCatt
1
timeit('for _ in [0]*100: pass',number=1000000) 的结果是 1.6880948543548584,而 timeit('for _ in [None]*100: pass',number=1000000) 的结果是 1.6721088886260986,还有 timeit('for _ in ['']*100: pass',number=1000000) 的结果是 0.13373517990112305 - 真棒! - camz
2
另外需要注意的是,在Python中幂运算符是**(例如10**1000),而^表示二进制异或,因此10^1000的结果为994 - hamstergene
2
我假设你在使用 10^1000 时并不是想要异或(它等于 994),而是想要幂运算:**(它等于一个后面跟着 1000 个零的数字 1)。 - Navith
显示剩余14条评论
2个回答

10

你的问题在于你没有正确地给timeit提供输入。

你需要给timeit提供包含Python语句的字符串。如果你没有这样做,

stmt = 'for _ in ['']*100: pass'

观察stmt的值。方括号内的引号字符匹配了字符串定界符,因此它们被Python解释为字符串定界符。由于Python会连接相邻的字符串文本,你会发现你实际上拥有的是'for _ in [' + ']*100: pass''for _ in []*100: pass'相同,即只是在空列表上进行循环,而不是一个包含100个元素的列表。尝试使用例如以下代码来运行测试:

stmt = 'for _ in [""]*100: pass'

读了几遍后才明白实际问题是引号。你能澄清一下吗?谢谢! - Paco
冒着让这个有用/好的答案变得毫无意义的风险,也许问题应该改回来,或者修改以包括两个版本。原始问题不再被回答了,为什么[0]*100(或类似的任何内容)比range(100)更快? - camz
@Dunedubby 速度略有差异是可以理解的,因为 range 必须创建 nint 对象,而 iter([None] * n) 可以使用 C 端整数进行循环。 - Veedrac
@camz:因为正如Veedrac所说,不需要创建新的整数对象(range()会按需生成整数)。 - Martijn Pieters

10
当迭代range()时,会生成0到n之间所有整数的对象;即使已缓存小整数,这也需要一定的时间。
另一方面,循环[None] * n则会产生n个对同一对象的引用,并且创建该列表会更快一些。
然而,range()对象使用的内存要少得多,而且更易读,这就是为什么人们喜欢使用它的原因。大多数代码不必追求极致的性能。
如果你需要那种速度,可以使用一个不占用内存的自定义可迭代对象,使用itertools.repeat()并带上第二个参数。
from itertools import repeat

for _ in repeat(None, n):

关于您的时间测试,存在一些问题。
首先,在您的 ['']*n 时间循环中,您犯了一个错误;您没有嵌入两个引号,而是连接了两个字符串并生成了一个空列表。
>>> '['']*n'
'[]*n'
>>> []*100
[]

我认为在迭代中,你迭代了0次,所以这是无敌的。

此外,您没有使用大数; ^ 是二进制异或运算符,而不是幂运算符:

>>> 10^1000
994

这意味着你的测试忽略了创建一个空值列表所需的时间。使用更好的数字和None可以得到:
>>> from timeit import timeit
>>> 10 ** 6
1000000
>>> timeit("for _ in range(10 ** 6): pass", number=100)
3.0651066239806823
>>> timeit("for _ in [None] * (10 ** 6): pass", number=100)
1.9346517859958112
>>> timeit("for _ in repeat(None, 10 ** 6): pass", 'from itertools import repeat', number=100)
1.4315521717071533

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接