web-dev-qa-db-de.com

Warum haben zwei identische Listen einen unterschiedlichen Speicherbedarf?

Ich habe zwei Listen erstellt l1 und l2, aber jede mit einer anderen Erstellungsmethode:

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

Aber die Ausgabe hat mich überrascht:

Size of l1 = 144
Size of l2 = 192

Die mit einem Listenverständnis erstellte Liste hat eine größere Speicherkapazität, die beiden Listen sind jedoch in Python ansonsten identisch.

Warum ist das so? Ist das eine interne Sache von CPython oder eine andere Erklärung?

146
Andrej Kesely

Wenn Sie [None] * 10 Schreiben, weiß Python), dass es eine Liste von genau 10 Objekten benötigt, also weist es genau das zu.

Wenn Sie ein Listenverständnis verwenden, weiß Python nicht, wie viel es benötigt. Die Liste wird also schrittweise erweitert, wenn Elemente hinzugefügt werden. Für jede Neuzuweisung wird mehr Platz zugewiesen, als sofort benötigt wird. Damit es nicht für jedes Element neu zugeordnet werden muss, ist die resultierende Liste wahrscheinlich etwas größer als erforderlich.

Sie können dieses Verhalten sehen, wenn Sie Listen vergleichen, die mit ähnlichen Größen erstellt wurden:

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

Sie sehen, dass die erste Methode genau das zuweist, was benötigt wird, während die zweite Methode periodisch wächst. In diesem Beispiel werden 16 Elemente zugewiesen, die beim Erreichen des 17. Elements neu zugeordnet werden mussten.

157
interjay

Wie in diese Frage vermerkt, verwendet das Listenverständnis list.append Unter der Haube, so dass es die Methode zur Größenänderung der Liste aufruft, die insgesamt zuteilt.

Um dies selbst zu demonstrieren, können Sie den Dissasembler dis verwenden:

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

Beachten Sie den Opcode LIST_APPEND Beim Disassemblieren des Codeobjekts <listcomp>. Aus dem docs :

LIST_APPEND (i)

Ruft list.append(TOS[-i], TOS) auf. Wird zum Implementieren von Listenverständnissen verwendet.

Nun haben wir für die Listenwiederholungsoperation einen Hinweis darauf, was los ist, wenn wir berücksichtigen:

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

Es scheint also möglich zu sein, genau die Größe zuzuweisen. Wenn wir uns Quellcode ansehen, sehen wir, dass genau das passiert:

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

Nämlich hier: size = Py_SIZE(a) * n;. Die restlichen Funktionen füllen einfach das Array aus.

47

Keine ist ein Speicherblock, aber keine vorgegebene Größe. Darüber hinaus gibt es in einem Array einen zusätzlichen Abstand zwischen den Array-Elementen. Sie können dies selbst sehen, indem Sie Folgendes ausführen:

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

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

Was nicht die Größe von l2 ergibt, sondern weniger ist.

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

Und das ist viel größer als ein Zehntel der Größe von l1.

Ihre Nummern sollten abhängig von den Details Ihres Betriebssystems und den Details der aktuellen Speichernutzung in Ihrem Betriebssystem variieren. Die Größe von [Keine] kann niemals größer sein als der verfügbare benachbarte Speicher, in dem die Variable gespeichert werden soll, und die Variable muss möglicherweise verschoben werden, wenn sie später dynamisch größer zugewiesen wird.

3
StevenJD