python中的堆栈/列表 - 它是如何追加的?

时间:2018-03-12 20:36:08

标签: python arrays list python-internals

如果我有一个清单:

list_1 = ["apples", "apricots", "oranges"]

我在列表中添加了一个新项目:"浆果"

list_1 = ["apples", "apricots", "oranges", "berries"]

引擎盖下(可以这么说),我以为我记得读过Python创建另一个列表(list_2)并将其指向原始列表(list_1),以便list_1保持静态...如果这是真的,它看起来像这样(引擎盖下)?

list_1 = ["apples", "apricots", ["oranges", "berries"]]

因此,通过这种方式,原始列表保持其大小。这是正确的看待方式吗?

4 个答案:

答案 0 :(得分:4)

在幕后,Python列表对象使用的C 数组结构更大;它是预先确定的大小。 Python列表的长度只是一个整数值,记录了数组中存储的Python元素数量。将一个元素附加到列表只是使用数组中的下一个空位,并且大小整数增加一。

当C数组中没有足够的空间时,会分配更多的内存来增长数组。如果删除元素,只使用一半数组,则会再次释放内存。

您可以在Python源代码的Objects/listobject.c file中看到实现。调整大小发生在list_resize() function,其中下面的代码片段决定新数组应该有多大,以便在内存使用情况(使用未使用的数组中的一堆指针)之间取得平衡,并避免必须复制数组太频繁了:

/* This over-allocates proportional to the list size, making room
 * for additional growth.  The over-allocation is mild, but is
 * enough to give linear-time amortized behavior over a long
 * sequence of appends() in the presence of a poorly-performing
 * system realloc().
 * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
 */
new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);

new_allocated 已添加到当前分配。因此,当您需要更多空间时,新大小除以8,加上3或6,指示在最小所需大小之上添加的额外元素数量。将元素附加到大小为1000的列表会添加131个额外插槽的缓冲区,而将元素附加到列表大小10只会增加额外的7个插槽。

从Python代码的角度来看,列表只是一系列索引,它们根据需要增长和缩小以适应所有元素。此处不涉及额外的列表,从视图中隐藏调整大小时数组的交换。

答案 1 :(得分:3)

不,在引擎盖下,列表由(通常)未充分利用的数组支持。

list1 -> [ x | x |  ]
           |   |
           |   v
           |   "apricots"
           v
           "apples"

当您追加list1时,只需更改第一个未使用的数组插槽的值:

list1 -> [ x | x | x ]
           |   |   |
           |   |   v
           |   |   "oranges" 
           |   v   
           |   "apricots"
           v
           "apples"

next 追加上,在添加新元素之前,会向阵列添加更多内存(并且需要更多内存)。 [一旦检测到阵列已满,就可以分配额外的内存;我不记得确切的细节。]

list1 -> [ x | x | x |  |  |  |  ]
           |   |   |
           |   |   v
           |   |   "oranges" 
           |   v   
           |   "apricots"
           v
           "apples"

list1 -> [ x | x | x | x |  |  |  ]
           |   |   |   |
           |   |   |   v
           |   |   |   "berries"
           |   |   v
           |   |   "oranges" 
           |   v   
           |   "apricots"
           v
           "apples"

实际分配的数量可能会有所不同,但所需的效果是appends的任何序列都具有常量操作的外观,即使每个人append可以是非常小的恒定时间操作或线性时间操作。然而,不变的是,你永远不会有太多的&#34;对象生命周期内的线性时间操作,保留每个append摊销运行时间。

答案 2 :(得分:3)

不,当你致电append时,Python会创建另一个列表。它就地改变现有列表。你可以很容易地看到这个:

>>> lst1 = []
>>> lst2 = lst1
>>> lst1.append(0)
>>> lst1
[0]
>>> lst2
[0]

如果您想创建另一个列表,可以改为:

>>> lst1 = []
>>> lst2 = lst1
>>> lst1 = lst1 + [0]
>>> lst1
[0]
>>> lst2
[]

那么,就地附加工作如何? Aren列出了引擎盖下的阵列?对,他们是。 Python在最后留下了一点空间,但是如果你append有足够的时间,它必须为列表分配一个新数组,移过所有元素,然后删除旧数组。它仍然是相同的列表对象,但在引擎盖下有不同的数组。

这种增长并不是每次只添加一个新插槽 - 这意味着每个append必须重新分配整个列表,因此追加将采用平均线性时间。相反,它增加了长度。像这样:

new_capacity = max(4, capacity * 8 // 5, new_length)

new_length就是为了让你一次性extend列出一大堆元素。)

通过几何而不是算术扩展,我们可以保证,虽然少数append确实需要线性时间,但足够的时间是分摊的时间是恒定的。你使用的究竟是什么因素是速度(高数字意味着更少的重新分配)和空间(更高的数字意味着更多的浪费空间)之间的权衡。我不知道CPython做了什么,但你可以在下面链接的源代码中找到它。大多数系统使用介于1.5和2.0之间的值(通常是一小部分小数字,因此它们可以进行整数倍和除数)。

如果你真的想要理解这一点,并且你可以遵循基本的C,你可以在listobject.hlistobject.c看看。您可能希望首先阅读C API文档,但这里是基础知识(类似于Python的伪代码,故意使用的不是真正的函数和字段名称):

if lst.size + 1 > lst.allocated:
    new_capacity = <see above>
    lst.array = PyRealloc(<enough memory for new_capacity pointers>)
    lst.allocated = new_capacity
incref(new_item)
lst.array[lst.size] = new_item
lst.size += 1

Realloc函数将成为平台函数的一个薄包装器,它将尝试找到更多的就地空间,但是回退到分配一个全新的指针并移动到所有的内容。

由于您使用的是Python,因此您很有可能成为喜欢通过互动实验学习的人。如果您不了解ctypes.pythonapi。你一定要开始玩它。您可以从Python内部的C API调用几乎任何内容。遗憾的是,您无法调用#define宏,或者在没有额外工作的情况下深入了解结构 - 但请参阅superhackyinternals了解如何进行额外的工作。 (我不认为我在列表中包含了任何内容,但是看一下int是如何工作的,你应该能够从那里得到它 - 只是不要看字符串,因为它们是一个更复杂。)当然,从你的翻译中解决这些问题,你会经常发生段错误,所以不要在你有重要历史的会话中这样做。

当然,对于每个Python实现,都不能保证是真的。只要实现可以提供记录的接口和性能特征,它就可以构建它想要的列表。例如,IronPython可能在.NET类库中使用了一些向量类。当然,这个课程会在自己的引擎下进行类似的重新分配和移动,但是IronPython并不关心它是如何做到的(并且你会更加关心)。

答案 3 :(得分:1)

Python实现可以做任何事情,只要它具有正确的行为。良好的实现也至少与the recommended time complexities一样快。

通常,如果可能,附加到列表会修改列表。在其append implementation中,如果没有更多空间,广泛使用的cpython resizes the list if necessary to 9/8 * old_size + 6。调整大小是通过保留更多内存(如果幸运)或分配新内存并复制所有旧元素来完成的。这意味着很少需要调整大小,特别是如果列表很大。大多数情况下,可以使用其中一个保留存储空间。