Python 预分配一个无的列表

Python 预分配一个无的列表,python,performance,list,design-patterns,python-3.x,Python,Performance,List,Design Patterns,Python 3.x,假设您想编写一个函数来生成一个对象列表,并且您预先知道该列表的长度n 在python中,列表支持O(1)中的索引访问,因此可以认为预先分配列表并使用索引访问它是一个好主意,而不是分配一个空列表并使用append()方法。这是因为如果空间不够,我们可以避免扩展整个列表的负担 如果我使用的是python,那么性能在任何情况下都可能没有那么重要,但是预分配列表的更好方法是什么呢 我知道这些可能的候选人: [None]*n→ 分配两个列表 [范围(n)中的x无]-或python2中的xrange→ 构

假设您想编写一个函数来生成一个对象列表,并且您预先知道该列表的长度
n

在python中,列表支持O(1)中的索引访问,因此可以认为预先分配列表并使用索引访问它是一个好主意,而不是分配一个空列表并使用
append()
方法。这是因为如果空间不够,我们可以避免扩展整个列表的负担

如果我使用的是python,那么性能在任何情况下都可能没有那么重要,但是预分配列表的更好方法是什么呢

我知道这些可能的候选人:

  • [None]*n
    → 分配两个列表
  • [范围(n)中的x无]
    -或python2中的
    xrange
    → 构建另一个对象
一个明显比另一个好吗


如果我们的情况是
n=len(输入)
?既然
input
已经存在,那么
[None for x in input]
会有更好的性能吗w.r.t.
[None]*len(input)

当您将一个项目附加到列表时,Python“over allocates”,请参见列表对象的。这意味着,例如,当向包含8个项目的列表中添加1个项目时,实际上会为8个新项目腾出空间,并且只使用其中的第一个项目。接下来的7个附件是“免费”的

在许多语言中(例如旧版本的Matlab,较新的JIT可能更好),您总是被告知需要预先分配向量,因为在循环过程中追加向量非常昂贵。在最坏的情况下,将单个项目添加到长度
n
的列表中可能需要
O(n)
时间,因为您可能需要创建一个更大的列表并复制所有现有项目。如果每次迭代都需要这样做,那么添加
n
项的总成本是
O(n^2)
,哎哟。Python的预分配方案将增加数组的成本分散到多个单个附加上(请参见),有效地降低了单个附加的成本
O(1)
和添加
n
O(n)

此外,Python代码的其余部分的开销通常非常大,通过预分配可以获得的微小加速是微不足道的。所以在大多数情况下,只需忘记预分配,除非您的探查器告诉您附加到列表是一个瓶颈


其他答案显示了列表预分配本身的一些分析,但这是无用的。唯一重要的是分析完整的代码,将所有计算都放在循环中,有无预分配。如果我的预测是正确的,那么差异是如此之小,以至于您赢得的计算时间与思考、编写和维护额外行以预分配列表所花费的时间相比相形见绌。

在这两个选项之间,第一个选项显然更好,因为不涉及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
也有一个属性,如果将
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

显然,第一个版本。让我解释一下原因

  • 当您执行
    [None]*n
    操作时,Python会在内部创建一个大小为
    n
    的列表对象,并将同一对象复制到所有内存位置(此处为
    None
    )(这就是原因,您应该仅在处理不可变对象时使用此方法)。因此,内存分配只执行一次。之后,通过列表进行一次迭代,将对象复制到所有元素。是与此类型的列表创建相对应的函数

    # 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;
    }
    
    #创建指定大小的列表
    np=(PyListObject*)PyList_新(尺寸);
    ....
    ...
    物料=np->ob\U物料;
    如果(Py_尺寸(a)==1){
    elem=a->ob_项[0];
    对于(i=0;i
  • 当您使用列表理解来构建列表时,Python无法知道所创建列表的实际大小,因此它最初会分配一块内存,并在列表中存储对象的新副本。当列表超出分配的长度时,它必须再次分配内存,并继续创建新对象并将其存储在列表中


  • 您是对的,这不太可能是您的性能限制。如果您遇到性能问题,请分析您的代码。瓶颈很少出现在您认为的地方。另请参阅源代码中提到的增长模式
    0,4,8,16,25,35,46,58,72,88,
    ,因此它可能是二次型(或任何您称之为二次型)而不是指数型。那么我的误解是。。。不记得我在哪里读到的。我将删除评论。它是指数型的,只是一个非常慢的评论。主要增长率为(1.125)^k,但在注释中显示的序列中,增长往往由固定贡献(+3/+6)控制。一些非常好的答案。我接受这一点,因为它实际上提供了一些数据,以及将方法保存到变量中的有趣想法。对其他答案也投了赞成票。您使用变量名
    \uu
    有什么特殊原因吗?@noot101没有特殊原因。