为什么list()和[]之间的getsizeof结果不同

时间:2019-02-21 13:08:39

标签: python list python-internals

在工作时,我发现了一件奇怪的事情:

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个字节的差异,我无法真正理解它们的来源。

当然,这与内部结构有关吗,但是任何人都可以向我解释一下幕后情况吗?

1 个答案:

答案 0 :(得分:4)

TL; DR:列出过度分配的列表,因此它们可以提供摊销的固定时间(O(1))附加操作。过度分配的数量取决于列表的创建方式以及实例的追加/删除历史记录。列表文学总是事先知道大小,并且不会过度分配(或仅分配很少)。 list函数并不总是知道结果的长度,因为它必须迭代参数,因此最终的过度分配取决于所使用的(与实现有关的)过度分配方案。

要了解我们正在查看的内容,重要的是要知道sys.getsizeof仅报告实例的大小。它不查看实例的内容。 因此,内容的大小(在这种情况下为int个)不予考虑。

实际上有助于列表大小的是(假设使用64位系统):

  • 8字节:引用计数。
  • 8字节:指向类的指针。
  • 8字节:存储列表中的元素数(相当于len(your_list))。
  • 8字节:存储用于保存列表中元素的数组的大小(这是len(your_list) + over_allocation)。
  • 8字节:指向存储指向内容的指针的数组的指针。
  • 列表的每个插槽8字节:用于保存指向列表中每个元素的指针(或NULL)。

  • 24个字节:需要其他内容(我认为是垃圾回收)

这种解释可能有点难以理解,因此如果我添加一些图像(忽略了用于垃圾收集的多余24个字节),它也许会变得更加清晰。我根据在CPython 3.7.2 Windows 64位和Anaconda的Python 64位上的发现创建了它们。

没有过度分配,例如为mylist = [1,2,3]

enter image description here

过度分配,例如为mylist = list([1,2,3])

enter image description here

或者对于手册appends

mylist = []
mylist.append(1)
mylist.append(2)
mylist.append(3)

enter image description here

这意味着一个空列表已经占用了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),而不是如何实现以及列表实例需要多少内存。