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

And*_*ely 148 python memory-management list python-internals

我创建了两个名单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))
Run Code Online (Sandbox Code Playgroud)

但输出让我感到惊讶:

Size of l1 = 144
Size of l2 = 192
Run Code Online (Sandbox Code Playgroud)

使用列表推导创建的列表在内存中的大小更大,但是这两个列表在Python中是相同的.

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

int*_*jay 158

当你编写时[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
Run Code Online (Sandbox Code Playgroud)

您可以看到第一种方法只分配所需的内容,而第二种方法则定期增长.在这个例子中,它为16个元素分配足够的元素,并且在到达17日时必须重新分配.

  • @AndrejKesely只在列表中使用带有不可变`x`的`[x]*n`.结果列表将保存对相同对象的引用. (26认同)
  • @ juanpa.arrivillaga真的,可能是.但通常它不是,特别是SO充满了海报,想知道为什么他们的所有数据同时改变:D (18认同)
  • @schwobaseggl嗯,那*可能*是你想要的,但很高兴理解这一点. (5认同)
  • 是的,这是有道理的。当我知道前面的大小时,最好用 `*` 创建列表。 (2认同)

jua*_*aga 48

正如在这个问题中所指出的那样,list-comprehension list.append在引擎盖下使用,因此它将调用list-resize方法,该方法进行了全面的分配.

为了向您自己演示,您实际上可以使用disdissasembler:

>>> 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
>>>
Run Code Online (Sandbox Code Playgroud)

注意代码对象LIST_APPEND反汇编中的操作<listcomp>码.来自文档:

LIST_APPEND(I)

打电话list.append(TOS[-i], TOS).用于实现列表推导.

现在,对于列表重复操作,如果我们考虑,我们会有一个提示:

>>> import sys
>>> sys.getsizeof([])
64
>>> 8*10
80
>>> 64 + 80
144
>>> sys.getsizeof([None]*10)
144
Run Code Online (Sandbox Code Playgroud)

所以,似乎能够准确地分配大小.看一下源代码,我们看到这正是发生的事情:

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);
Run Code Online (Sandbox Code Playgroud)

即,这里:size = Py_SIZE(a) * n;.其余的函数只是填充数组.

  • @Acccumulation这是不正确的.`list.append`是一个摊销的常量时间操作,因为当列表调整大小时,它会进行分配.因此,并非每个追加操作都会产生新分配的数组.无论如何,我链接的问题在源代码中向您显示实际上,列表推导*do*使用`list.append`,.我马上回到我的笔记本电脑,我可以向你展示列表理解的反汇编字节码和相应的`LIST_APPEND`操作码 (7认同)