假设您要编写一个产生对象列表的函数,并且您事先知道此列表的长度n
。
在python中,列表支持O(1)中的索引访问,因此可以预先分配列表并使用索引访问它而不是分配空列表并使用append()
方法。这是因为如果空间不够,我们可以避免扩展整个列表的负担。
如果我正在使用python,那么在任何情况下表演都不是那么相关,但是预先分配列表的更好方法是什么?
我知道这些可能的候选人:
[None] * n
→分配两个列表[None for x in range(n)]
- 或python2中的xrange
→构建另一个对象一个明显优于另一个吗?
如果我们处于n = len(input)
的情况怎么办?由于input
已经存在,[None for x in input]
会有更好的表现吗? [None] * len(input)
?
答案 0 :(得分:15)
当您将项目追加到列表中时,Python'过度分配',请参阅列表对象的source-code。这意味着,例如,当将1个项目添加到8个项目的列表中时,它实际上为8个新项目腾出空间,并且仅使用其中的第一个项目。接下来的7个追加是“免费”。
在许多语言(例如Matlab)中,总是告诉您需要预先分配向量,因为在循环期间追加非常昂贵。在最糟糕的情况下,将单个项目附加到长度为n
的列表可能会花费O(n)
时间,因为您可能需要创建更大的列表并复制所有现有项目。您需要在每次迭代时执行此操作,因此添加n
项的总成本为O(n^2)
,哎哟。 Python的预分配方案通过许多单个附加扩展了阵列的成本(参见amortized costs),有效地降低了单个附加O(1)
的成本以及添加n
项的总成本O(n)
。
在Python中,其余代码的开销通常很大,通过预分配可以获得的微小加速是微不足道的。因此,在大多数情况下,只需忘记预分配,除非您的分析器告诉您附加到列表是瓶颈。
其他答案显示了列表预分配本身的一些分析,但这没用。唯一重要的是分析您的完整代码,在循环中进行所有计算,无论是否进行预分配。如果我的预测是正确的,那么差异是如此之小,以至于您赢得的计算时间与考虑,编写和维护额外行以预先分配列表所花费的时间相比相形见绌。
答案 1 :(得分:14)
在这两个选项之间,第一个显然更好,因为没有涉及Python for循环。
>>> %timeit [None] * 100
1000000 loops, best of 3: 469 ns per loop
>>> %timeit [None for x in range(100)]
100000 loops, best of 3: 4.8 us per loop
<强>更新强>
list.append
也有O(1)
complexity,如果您将list.append
方法分配给变量,那么它可能是比预创建列表更好的选择。
>>> n = 10**3
>>> %%timeit
lis = [None]*n
for _ in range(n):
lis[_] = _
...
10000 loops, best of 3: 73.2 us per loop
>>> %%timeit
lis = []
for _ in range(n):
lis.append(_)
...
10000 loops, best of 3: 92.2 us per loop
>>> %%timeit
lis = [];app = lis.append
for _ in range(n):
app(_)
...
10000 loops, best of 3: 59.4 us per loop
>>> n = 10**6
>>> %%timeit
lis = [None]*n
for _ in range(n):
lis[_] = _
...
10 loops, best of 3: 106 ms per loop
>>> %%timeit
lis = []
for _ in range(n):
lis.append(_)
...
10 loops, best of 3: 122 ms per loop
>>> %%timeit
lis = [];app = lis.append
for _ in range(n):
app(_)
...
10 loops, best of 3: 91.8 ms per loop
答案 2 :(得分:2)
执行[None] * n
时,Python会在内部创建一个大小为n
的列表对象,并且复制同一个对象(此处为None
) (这就是你应该在处理不可变对象时使用此方法)到所有内存位置的原因。所以内存分配只进行一次。之后,通过列表进行单次迭代,将对象复制到所有元素。 list_repeat
是与此类列表创建相对应的函数。
# Creates the list of specified size
np = (PyListObject *) PyList_New(size);
....
...
items = np->ob_item;
if (Py_SIZE(a) == 1) {
elem = a->ob_item[0];
for (i = 0; i < n; i++) {
items[i] = elem; // Copies the same item
Py_INCREF(elem);
}
return (PyObject *) np;
}
当您使用列表推导来构建列表时,Python无法知道正在创建的列表的实际大小,因此它最初会分配一块内存和一个新副本对象存储在列表中。当列表超出分配的长度时,它必须再次分配内存并继续创建新对象并将其存储在列表中。