如果我有一个清单:
list_1 = ["apples", "apricots", "oranges"]
我在列表中添加了一个新项目:"浆果"
list_1 = ["apples", "apricots", "oranges", "berries"]
引擎盖下(可以这么说),我以为我记得读过Python创建另一个列表(list_2)并将其指向原始列表(list_1),以便list_1保持静态...如果这是真的,它看起来像这样(引擎盖下)?
list_1 = ["apples", "apricots", ["oranges", "berries"]]
因此,通过这种方式,原始列表保持其大小。这是正确的看待方式吗?
答案 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.h
和listobject.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
。调整大小是通过保留更多内存(如果幸运)或分配新内存并复制所有旧元素来完成的。这意味着很少需要调整大小,特别是如果列表很大。大多数情况下,可以使用其中一个保留存储空间。