为什么两个完全相同的列表占用的内存不同?

170

我创建了两个列表l1l2,但是每个列表都使用不同的创建方法:

import sys

l1 = [None] * 10
l2 = [None for _ in range(10)]

print('Size of l1 =', sys.getsizeof(l1))
print('Size of l2 =', sys.getsizeof(l2))

但输出结果让我感到惊讶:

Size of l1 = 144
Size of l2 = 192

使用列表推导创建的列表在内存中占用更大的空间,但在Python中这两个列表在其他方面是相同的。

为什么会这样?这是一些CPython内部的东西,还是其他的解释?


2
可能,重复运算符将调用一些函数来精确地确定底层数组的大小。请注意,其中8是指针的大小,(144 == sys.getsizeof([]) + 8*10) - juanpa.arrivillaga
1
请注意,如果您将10更改为11,则[None] * 11列表的大小为152,但列表推导式的大小仍为192。之前链接的问题不是完全重复的,但它有助于理解为什么会发生这种情况。 - Patrick Haugh
3个回答

182
当你写 [None] * 10 时,Python 知道它将需要一个恰好包含 10 个对象的列表,因此它会精确地分配这么多空间。
当你使用列表推导时,Python 不知道需要多少空间。所以它会在元素添加时逐渐增加列表大小。为了避免每次添加元素都要重新分配空间,它会在每次重新分配时多分配一些空间。因此,结果列表可能会比实际需要的大一些。
你可以通过对比创建类似大小的列表来观察这种行为:
>>> sys.getsizeof([None]*15)
184
>>> sys.getsizeof([None]*16)
192
>>> sys.getsizeof([None for _ in range(15)])
192
>>> sys.getsizeof([None for _ in range(16)])
192
>>> sys.getsizeof([None for _ in range(17)])
264

你可以看到第一种方法只分配所需的内存,而第二种方法会定期增加内存。在此示例中,它为16个元素分配了足够的内存,并在达到第17个元素时进行了重新分配。


2
是的,这很有道理。当我知道大小时,最好使用 * 创建列表。 - Andrej Kesely
28
只有在列表中使用不可变的 x 才能使用 [x] * n。生成的列表将保存对相同对象的引用。 - user2390182
5
@schwobaseggl,嗯,那可能是你想要的,但了解这一点很重要。 - juanpa.arrivillaga
21
@juanpa.arrivillaga 是的,可能是这样。但通常情况下并不是这样的,特别是在 Stack Overflow 上,很多发帖者都在想为什么他们的所有数据同时更改了 :D - user2390182

57

正如在这个问题中所指出的,列表推导式在幕后使用list.append,因此它将调用列表调整大小方法,进行过量分配。

要自行证明这一点,您可以实际使用dis反汇编器:

>>> code = compile('[x for x in iterable]', '', 'eval')
>>> import dis
>>> dis.dis(code)
  1           0 LOAD_CONST               0 (<code object <listcomp> at 0x10560b810, file "", line 1>)
              2 LOAD_CONST               1 ('<listcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_NAME                0 (iterable)
              8 GET_ITER
             10 CALL_FUNCTION            1
             12 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x10560b810, file "", line 1>:
  1           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                 8 (to 14)
              6 STORE_FAST               1 (x)
              8 LOAD_FAST                1 (x)
             10 LIST_APPEND              2
             12 JUMP_ABSOLUTE            4
        >>   14 RETURN_VALUE
>>>

<listcomp>代码对象的反汇编中注意LIST_APPEND操作码。从文档中得知:

LIST_APPEND(i)

调用list.append(TOS[-i], TOS),用于实现列表推导。

现在,对于列表重复操作,如果我们考虑以下内容,则可以了解正在发生的情况:

>>> import sys
>>> sys.getsizeof([])
64
>>> 8*10
80
>>> 64 + 80
144
>>> sys.getsizeof([None]*10)
144

因此,看起来可以精确地分配大小。查看源代码,我们可以看到这正是发生的事情:

static PyObject *
list_repeat(PyListObject *a, Py_ssize_t n)
{
    Py_ssize_t i, j;
    Py_ssize_t size;
    PyListObject *np;
    PyObject **p, **items;
    PyObject *elem;
    if (n < 0)
        n = 0;
    if (n > 0 && Py_SIZE(a) > PY_SSIZE_T_MAX / n)
        return PyErr_NoMemory();
    size = Py_SIZE(a) * n;
    if (size == 0)
        return PyList_New(0);
    np = (PyListObject *) PyList_New(size);
即,在这里:size = Py_SIZE(a) * n;。其余函数只是填充数组。

正如这个问题中所指出的,列表推导式在幕后使用了list.append。我认为更准确的说法是它使用了.extend() - Acccumulation
@Acccumulation 你为什么这样认为? - juanpa.arrivillaga
因为它不是逐个添加元素。当您将元素附加到列表时,实际上正在创建一个新列表,并在该新内存分配中放置该列表。另一方面,列表推导式将大部分新元素放入已经分配的内存中,当它们用完分配的内存时,它们会分配另一个内存块,而不仅仅是足够新元素的内存。 - Acccumulation
8
@Acccumulation 这是不正确的。list.append 是摊销常数时间操作,因为当列表调整大小时,它会过度分配空间。因此,并不是每次附加操作都会导致新的分配数组。无论如何,我链接的问题在源代码中显示了 list comprehensions do 使用 list.append。我马上回到我的电脑上,并可以展示解释一下列表推导式的反汇编字节码和相应的 LIST_APPEND 操作码。 - juanpa.arrivillaga

2

None是一块内存,但它没有预先指定的大小。除此之外,在数组元素之间还有一些额外的空间。您可以通过运行以下命令自行查看:

for ele in l2:
    print(sys.getsizeof(ele))

>>>>16
16
16
16
16
16
16
16
16
16

这并不是计算L2大小的总和,而是小于其总和。

print(sys.getsizeof([None]))
72

这比 l1 大十分之一还要多。

你的数字应该根据操作系统的细节和当前内存使用情况而变化。[None] 的大小永远不能超过变量被设置存储的可用相邻内存的大小,如果变量后来被动态分配为更大,则可能必须将其移动。


2
None实际上并没有存储在底层数组中,唯一存储的是一个PyObject指针(8个字节)。所有Python对象都分配在堆上。None是单例,因此具有许多非值的列表只会创建指向堆上同一None对象的PyObject指针数组(并且不会在此过程中使用额外的内存以获得额外的None)。我不确定您所说的“None没有预先指定的大小”是什么意思,但这听起来不正确。最后,使用getsizeof每个元素的循环并不能展示您想展示的内容。 - juanpa.arrivillaga
如果你所说的是真的,那么[None]*10的大小应该与[None]的大小相同。但显然不是这样的——额外的存储空间已经被添加了。事实上,[None]重复十次的大小(160)也小于[None]乘以十的大小。正如你所指出的,[None]指针的大小明显比[None]本身要小(16字节而不是72字节)。然而,160+32等于192。我认为前面的答案也没有完全解决问题。很明显,一些额外的少量内存(可能是机器状态相关的)被分配了。 - StevenJD
如果你所说的是真的,那么 [None]*10 的大小应该与 [None] 的大小相同。我说了什么可能会暗示这一点呢?再次强调,你似乎集中在底层缓冲区被过度分配的事实上,或者列表的大小包括比底层缓冲区的大小更多的内容(当然它确实包括),但这不是这个问题的重点。再次说明,你对 l2 中每个 ele 使用 gestsizeof 是误导性的,因为 getsizeof(l2) 不考虑容器内部元素的大小 - juanpa.arrivillaga
为了证明这个说法,可以执行l1 = [None]; l2 = [None]*100; l3 = [l2],然后运行print(sys.getsizeof(l1), sys.getsizeof(l2), sys.getsizeof(l3))。你会得到类似于 72 864 72的结果。也就是说,在64位系统中,8字节指针大小下,分别为64 + 1*864 + 100*864 + 1*8 - juanpa.arrivillaga
2
正如我所说,sys.getsizeof *不考虑容器中项目的大小。从文档中可以看到:“只计算直接归因于对象的内存消耗,而不计算它所引用的对象的内存消耗...请参见递归大小示例,了解如何使用getsizeof()递归地查找容器及其所有内容的大小。” - juanpa.arrivillaga
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接