为什么range() - 函数比乘法项更慢以获取嵌套列表中的副本?

时间:2018-06-13 13:47:13

标签: python python-3.x timeit

要复制现有列表中的嵌套列表,遗憾的是仅仅将其相乘是不够的,否则将创建引用而不是列表中的独立列表,请参阅以下示例:

x = [[1, 2, 3]] * 2
x[0] is x[1]  # will evaluate to True

要实现目标,您可以在列表推导中使用范围功能,例如,请参阅:

x = [[1, 2, 3] for _ in range(2)]
x[0] is x[1]  # will evaluate to False (wanted behaviour)

这是一种在不创建引用的情况下将列表中的项目相乘的好方法,这也在许多不同的网站上多次解释。

但是,有一种更有效的方法来复制列表元素。那个代码对我来说似乎有点快(通过命令行的timeit和不同的参数n∈{1,50,100,10000}测量下面的代码和上面代码中的范围(n)):

x = [[1, 2, 3] for _ in [0] * n]

但我想知道,为什么这段代码运行得更快?还有其他缺点(更多的内存消耗或类似情况)?

python -m timeit '[[1, 2, 3] for _ in range(1)]'
1000000 loops, best of 3: 0.243 usec per loop

python -m timeit '[[1, 2, 3] for _ in range(50)]'
100000 loops, best of 3: 3.79 usec per loop

python -m timeit '[[1, 2, 3] for _ in range(100)]'
100000 loops, best of 3: 7.39 usec per loop

python -m timeit '[[1, 2, 3] for _ in range(10000)]'
1000 loops, best of 3: 940 usec per loop

python -m timeit '[[1, 2, 3] for _ in [0] * 1]'
1000000 loops, best of 3: 0.242 usec per loop

python -m timeit '[[1, 2, 3] for _ in [0] * 50]'
100000 loops, best of 3: 3.77 usec per loop

python -m timeit '[[1, 2, 3] for _ in [0] * 100]'
100000 loops, best of 3: 7.3 usec per loop

python -m timeit '[[1, 2, 3] for _ in [0] * 10000]'
1000 loops, best of 3: 927 usec per loop


# difference will be greater for larger n

python -m timeit '[[1, 2, 3] for _ in range(1000000)]'
10 loops, best of 3: 144 msec per loop

python -m timeit '[[1, 2, 3] for _ in [0] * 1000000]'
10 loops, best of 3: 126 msec per loop

1 个答案:

答案 0 :(得分:4)

这是正确的; range,即使在Python 3中,它生成一个紧凑的范围对象,在计算和存储之间的经典权衡中比列表更复杂。

由于列表变得太大而无法容纳在缓存中(如果我们关注性能的主要问题),范围对象会遇到另一个问题:当创建范围中的每个数字时,它会销毁并创建新的int个对象(前256个左右的成本较低,因为它们被实习,但它们之间的差异仍可能导致一些缓存未命中)。该列表将继续引用相同的列表。

但是,还有更有效的选择;例如,bytearray将消耗比列表少得多的内存。可能隐藏在itertoolsrepeat中的任务的最佳功能。像范围对象一样,它不需要存储所有副本,但是像重复列表一样,它不需要创建不同的对象。因此,像for _ in repeat(None, x)这样的东西只会戳到相同的几个缓存行(对象的迭代计数和引用计数)。

最后,人们坚持使用range的主要原因是因为它突出显示了什么(在固定计数循环的惯用语和内置函数中)。

在其他Python实现中,范围很可能比重复更快;这是因为计数器本身已经保持了价值。我期待Cython或PyPy的这种行为。