Почему два идентичных списка имеют разный объем памяти?

Я создал два списка l1 и l2, но каждый из них имеет другой метод создания:

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 или какое-то другое объяснение?

Ответы

Ответ 1

Когда вы пишете [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

Как отмечалось в этом вопросе, в представлении списка используется list.append под капотом, поэтому он вызовет метод list-resize, который будет зависеть.

Чтобы продемонстрировать это себе, вы действительно можете использовать dis dissasembler:

>>> 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
>>>

Обратите внимание на код операции LIST_APPEND при разборке объекта кода <listcomp>. Из документов:

LIST_APPEND (я)

Вызовы 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; , Остальные функции просто заполняют массив.

Ответ 3

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.

Ваши номера должны различаться в зависимости от деталей вашей операционной системы и деталей использования текущей памяти в вашей операционной системе. Размер [Нет] никогда не может быть больше, чем доступная смежная память, где переменная должна быть сохранена, и переменная, возможно, придется перемещать, если впоследствии динамически распределено ее больше.