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

col*_*yre 5 python timeit python-3.x

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

x = [[1, 2, 3]] * 2
x[0] is x[1]  # will evaluate to True
Run Code Online (Sandbox Code Playgroud)

为了实现目标,您可以在列表推导中使用范围函数,例如,请参阅:

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

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

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

x = [[1, 2, 3] for _ in [0] * n]
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

Yan*_*ier 5

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

当列表变得太大而无法放入缓存时(如果我们关心性能,这是主要问题),范围对象会遇到不同的问题:当创建范围中的每个数字时,它会销毁并创建新对象int(第一个对象) 256 左右的成本较低,因为它们是被保留的,但它们的差异仍然会导致一些缓存未命中)。该列表将继续引用同一个列表。

不过,还有更有效的选择;例如,字节数组消耗的内存比列表少得多。也许该任务的最佳功能隐藏在itertools:中repeat。与范围对象一样,它不需要存储所有副本,但与重复列表一样,它不需要创建不同的对象。因此,类似的东西for _ in repeat(None, x)只会刺探相同的几个缓存行(对象的迭代计数和引用计数)。

最后,人们坚持使用的主要原因range是因为它是突出呈现的(无论是在固定计数循环的习惯用法中还是在内置函数中)。

在其他Python实现中,range很可能比repeat更快;这是因为计数器本身已经保存了该值。我预计 Cython 或 PyPy 会出现这种行为。