为什么 [None] * 10 比 [None for i in range(10)] 更快?

4

我希望创建一个具有一些初始化值的列表,因为在Python中空列表不是一个选项。所以我开始考虑哪种方法更快:

l = [None for i in range(1000)] 或者 l = [None] * 1000

我尝试使用timeit进行测试:

In [56]: timeit.timeit('l = [None] * 1000', number=10000)
Out[56]: 0.04936316597741097
In [58]: timeit.timeit('l = [None for i in range(1000)]', number=10000)
Out[58]: 0.2318978540133685

我很惊讶[None] * 1000的速度更快。
  1. 为什么会这样 (我的性能测试方式是否正确) ?
  2. 有更快速的初始化“空”列表的方法吗?

4
l = [] 是一个“空”列表。 - Chris_Rands
不用理会我之前的评论。即使使用更好的测试参数,第一个还是比第二个快大约14倍,这个差距比你测试所显示的更加严重。 - Carcigenicate
5
无论如何,列表推导式本质上是一个解释器级别的for循环(带有一些优化)。它还使用.append,因此每隔一段时间,整个底层数组都会被重新调整大小。如果您查看使用*运算符进行列表重复的源代码,则会预先分配整个底层数组,从不需要调整大小,并且循环是在C级别完成的... - juanpa.arrivillaga
3
这是我写的一个回答,它与相关问题有关,可能会很有启发性。 - juanpa.arrivillaga
1
@zvone:算法中如果列表不是按照前后顺序填充,则通常需要一个虚拟值列表。 - Davis Herring
显示剩余6条评论
1个回答

3

我假设你正在使用CPython。让我们比较生成的Python字节码和dis模块。这是第一个版本:

>>> import dis
>>> def f():
...     return [None] * 1000
>>> dis.dis(f)
  2           0 LOAD_CONST               0 (None)
              2 BUILD_LIST               1
              4 LOAD_CONST               1 (1000)
              6 BINARY_MULTIPLY
              8 RETURN_VALUE

这很清楚:创建了一个列表[None](第0-2行),然后乘以1000(第4-6行)。
这是第二个版本:
>>> def g():
...     return [None for _ in range(1000)]
>>> dis.dis(g)
  2           0 LOAD_CONST               1 (<code object <listcomp> at ..., file "<doctest __main__[3]>", line 2>)
              2 LOAD_CONST               2 ('g.<locals>.<listcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_GLOBAL              0 (range)
              8 LOAD_CONST               3 (1000)
             10 CALL_FUNCTION            1
             12 GET_ITER
             14 CALL_FUNCTION            1
             16 RETURN_VALUE

这更加复杂:创建了一个名为g.<locals>.<listcomp>的函数(第2行), 并且有下面代码中会看到的代码(第0行) (第4行)。创建了一个range(1000)(第6-8-10行)和一个迭代器(第12行)。该迭代器被传递给g.<locals>.<listcomp>函数(第14行),并返回结果(第16行)。

让我们看一下g.<locals>.<listcomp>函数:

>>> dis.dis(g.__code__.co_consts[1])
  2           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                 8 (to 14)
              6 STORE_FAST               1 (_)
              8 LOAD_CONST               0 (None)
             10 LIST_APPEND              2
             12 JUMP_ABSOLUTE            4
        >>   14 RETURN_VALUE

创建一个空列表(第0行),将迭代器参数(iter(range(1000)))推送到堆栈上(第2行),然后开始一个for循环(第4行)。循环索引的值(_)存储在本地数组中(第6行),并且将None追加到列表中(第8-10行),直到循环结束(第12行回到第4行)。
总结一下:
  • 第一个版本:乘法;
  • 第二个版本:创建了一个本地函数,创建了一个范围并将迭代器传递给函数;此函数逐个迭代元素并逐个添加。
第二个版本确实较慢。
注意通常的陷阱。
>>> A = [[0]] * 3
>>> A
[[0], [0], [0]]
>>> A[0].append(1)
>>> A
[[0, 1], [0, 1], [0, 1]]

但是:
>>> A = [[0] for _ in range(3)]
>>> A
[[0], [0], [0]]
>>> A[0].append(1)
>>> A
[[0, 1], [0], [0]]

如果你想知道为什么,请看上面的字节码。

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