在工作时,我发现了一件奇怪的事情:
from sys import getsizeof as gs
list1=[1]
list2=list([1])
list1==list2 #true
gs(list1) #80. (I guess 72 overhead +8 of the int)
gs(list2) #104. (I guess 72 + 8 as above + 24 of...?)
list3=[1,2,3,4,5]
list4=list(list3)
gs(list3) #112
gs(list4) #136
因此,总是有24个字节的差异,我无法真正理解它们的来源。
当然,这与内部结构有关吗,但是任何人都可以向我解释一下幕后情况吗?
答案 0 :(得分:4)
TL; DR:列出过度分配的列表,因此它们可以提供摊销的固定时间(O(1)
)附加操作。过度分配的数量取决于列表的创建方式以及实例的追加/删除历史记录。列表文学总是事先知道大小,并且不会过度分配(或仅分配很少)。 list
函数并不总是知道结果的长度,因为它必须迭代参数,因此最终的过度分配取决于所使用的(与实现有关的)过度分配方案。
要了解我们正在查看的内容,重要的是要知道sys.getsizeof
仅报告实例的大小。它不查看实例的内容。 因此,内容的大小(在这种情况下为int
个)不予考虑。
实际上有助于列表大小的是(假设使用64位系统):
len(your_list)
)。len(your_list) + over_allocation
)。列表的每个插槽8字节:用于保存指向列表中每个元素的指针(或NULL)。
24个字节:需要其他内容(我认为是垃圾回收)
这种解释可能有点难以理解,因此如果我添加一些图像(忽略了用于垃圾收集的多余24个字节),它也许会变得更加清晰。我根据在CPython 3.7.2 Windows 64位和Anaconda的Python 64位上的发现创建了它们。
没有过度分配,例如为mylist = [1,2,3]
:
过度分配,例如为mylist = list([1,2,3])
:
或者对于手册appends
:
mylist = []
mylist.append(1)
mylist.append(2)
mylist.append(3)
这意味着一个空列表已经占用了64个字节,假设该空列表没有过度分配。对于每个添加的元素,必须添加对Python对象的另一个引用(指针为8个字节)。
所以list
的最小大小为:
size_min = 64 + 8 * n_items
Python列表是可变大小的,并且如果它仅分配尽可能多的空间来容纳当前数量的项目,则每当添加新项目(使其O(n)
)时,都必须复制整个数组。但是,如果分配过多,这意味着实际上占用的内存比存储元素所需的内存更多,那么您可以支持摊销的O(1)
追加,因为它有时仅需要调整大小。例如,请参见Wikipedia "Amortized analysis"。
接下来的一点是,文字总是知道其大小,您将x
项放入文字中,并且在源代码解析时,它已经知道列表必须有多大。因此,您只需为以下内容分配所需的内存即可:
l = [1, 2, 3]
但是,由于list
是可调用的,并且即使参数只是一个文字,Python也无法优化该调用(我的意思是您可以为名称list
分配其他名称),因此它具有真正 呼叫list
。
list
本身只是对参数进行迭代,并将项目追加到其内部数组中,在需要时调整大小并过度分配以使其摊销O(1)
。 list
可以检查输入的大小,但是(理论上)由于在迭代对象时可能发生任何事情,因此将长度估算作为一个粗略的指导而非保证。因此,尽管它可以预测参数中的项数,但它避免了重新分配,但仍然过度分配(以防万一)。
请注意,所有这些都是实施细节,在其他Python实现中,甚至在不同的CPython版本中,它也可能完全不同。 Python唯一保证的(我想是的,我不是100%肯定)是append
被摊销O(1)
,而不是如何实现以及列表实例需要多少内存。>