Was bewirkt, dass [* a] insgesamt zugeordnet wird?

136

Anscheinend list(a)nicht zuordnen, [x for x in a]an einigen Stellen insgesamt [*a]zuordnen und die ganze Zeit insgesamt zuordnen ?

Größen bis zu n = 100

Hier sind die Größen n von 0 bis 12 und die resultierenden Größen in Bytes für die drei Methoden:

0 56 56 56
1 64 88 88
2 72 88 96
3 80 88 104
4 88 88 112
5 96 120 120
6 104 120 128
7 112 120 136
8 120 120 152
9 128 184 184
10 136 184 192
11 144 184 200
12 152 184 208

So berechnet , reproduzierbar bei repl.it mit Python 3. 8 :

from sys import getsizeof

for n in range(13):
    a = [None] * n
    print(n, getsizeof(list(a)),
             getsizeof([x for x in a]),
             getsizeof([*a]))

Also: Wie funktioniert das? Wie [*a]ordnet man insgesamt zu? Welchen Mechanismus verwendet es tatsächlich, um die Ergebnisliste aus der angegebenen Eingabe zu erstellen? Verwendet es einen Iterator aund verwendet so etwas wie list.append? Wo ist der Quellcode?

( Colab mit Daten und Code , die die Bilder erzeugt haben.)

Vergrößern auf kleiner n:

Größen bis zu n = 40

Verkleinern auf größer n:

Größen bis zu n = 1000

Stefan Pochmann
quelle
1
Fwiw, wenn Sie Ihre Testfälle erweitern, scheint sich das Listenverständnis so zu verhalten, als würde man eine Schleife schreiben und jedes Element an die Liste anhängen, während es sich [*a]so zu verhalten scheint, als würde man extendeine leere Liste verwenden.
Jdehesa
4
Es kann hilfreich sein, den jeweils generierten Bytecode zu überprüfen. list(a)arbeitet vollständig in C; Es kann den internen Puffer Knoten für Knoten zuweisen, während er iteriert a. [x for x in a]verwendet nur LIST_APPENDviel, so folgt es dem normalen Muster "Gesamt ein wenig zuordnen, bei Bedarf neu zuweisen" einer normalen Liste. [*a]verwendet BUILD_LIST_UNPACK, die ... Ich weiß nicht, was das tut, außer anscheinend die ganze Zeit zu
viel
2
Außerdem scheint es in Python 3.7 so zu sein, dass list(a)und [*a]identisch sind und beide im Vergleich zu insgesamt zugeordnet sind [x for x in a], sodass ... sys.getsizeofmöglicherweise nicht das richtige Werkzeug für die Verwendung ist.
Chepper
7
@chepner Ich denke, es sys.getsizeofist das richtige Tool, es zeigt nur, dass list(a)für die Gesamtzuordnung verwendet. Eigentlich Was ist neu in Python 3.8 erwähnt es: „Die Liste Konstruktor nicht overallocate [...]“ .
Stefan Pochmann
5
@chepner: Das war ein Fehler, der in 3.8 behoben wurde ; Der Konstruktor soll nicht insgesamt zuordnen.
ShadowRanger

Antworten:

81

[*a] macht intern das C-Äquivalent von :

  1. Mach ein neues, leeres list
  2. Anruf newlist.extend(a)
  3. Rückgabe list.

Wenn Sie also Ihren Test erweitern auf:

from sys import getsizeof

for n in range(13):
    a = [None] * n
    l = []
    l.extend(a)
    print(n, getsizeof(list(a)),
             getsizeof([x for x in a]),
             getsizeof([*a]),
             getsizeof(l))

Probieren Sie es online aus!

Sie sehen die Ergebnisse für getsizeof([*a])und l = []; l.extend(a); getsizeof(l)sind gleich.

Dies ist normalerweise das Richtige. Wenn extendSie normalerweise erwarten, später mehr hinzuzufügen, und ähnlich wie beim allgemeinen Auspacken, wird davon ausgegangen, dass mehrere Dinge nacheinander hinzugefügt werden. [*a]ist nicht der normale Fall; Python geht davon aus, dass dem list( [*a, b, c, *d]) mehrere Elemente oder Iterables hinzugefügt werden , sodass die Gesamtzuordnung im allgemeinen Fall Arbeit spart.

Im Gegensatz dazu kann ein listaus einem einzigen, vordefinierten iterierbaren (mit list()) konstruiertes Element während des Gebrauchs nicht wachsen oder schrumpfen, und die Gesamtzuordnung ist verfrüht, bis das Gegenteil bewiesen ist. Python hat kürzlich einen Fehler behoben, durch den der Konstruktor auch für Eingaben mit bekannter Größe insgesamt zugeordnet wurde .

Das listVerständnis entspricht effektiv wiederholten appends, sodass Sie das Endergebnis des normalen Gesamtzuordnungswachstumsmusters sehen, wenn Sie jeweils ein Element hinzufügen.

Dies ist keine Sprachgarantie. Es ist nur so, wie CPython es implementiert. Die Python-Sprachspezifikation befasst sich im Allgemeinen nicht mit bestimmten Wachstumsmustern in list(abgesehen von der Garantie von amortisierten O(1) appends und pops vom Ende). Wie in den Kommentaren erwähnt, ändert sich die spezifische Implementierung in 3.9 erneut. Dies wirkt sich zwar nicht auf [*a]andere Fälle aus, in denen das, was früher "ein temporäres tupleElement erstellen und dann extendmit dem tuple" erstellt wurde, nun zu mehreren Anwendungen von wird LIST_APPEND, die sich ändern können, wenn die Gesamtzuordnung erfolgt und welche Zahlen in die Berechnung einfließen.

ShadowRanger
quelle
4
@StefanPochmann: Ich habe den Code schon einmal gelesen (weshalb ich das schon wusste). Dies ist der Bytecode-Handler fürBUILD_LIST_UNPACK , der _PyList_Extendals C-Äquivalent zum Aufruf verwendet wird extend(nur direkt und nicht durch Methodensuche). Sie kombinierten es mit den Wegen zum Bauen eines tuplemit Auspacken; tuples lassen sich für das stückweise Erstellen nicht gut zuordnen, daher entpacken sie immer zu a list(um von der Gesamtzuordnung zu profitieren) und konvertieren tupleam Ende zu, wenn dies angefordert wurde.
ShadowRanger
4
Beachten Sie, dass dies ändert sich offenbar in 3.9 , wo der Bau mit separatem Bytecode (getan wird BUILD_LIST, LIST_EXTENDfür jede Sache auszupacken, LIST_APPENDfür einzelne Artikel), statt Laden alles auf dem Stapel vor dem Bau der gesamten listmit einem einzigen Byte - Code - Anweisung (ermöglicht es dem Compiler auszuführen Optimierungen , dass der all-in-one Unterricht nicht erlauben, wie die Umsetzung [*a, b, *c]als LIST_EXTEND, LIST_APPEND, LIST_EXTENDw / o wickeln , um bin einem ein- tupledie Anforderungen gerecht zu werden BUILD_LIST_UNPACK).
ShadowRanger
18

Vollständiges Bild davon, was passiert, basierend auf den anderen Antworten und Kommentaren (insbesondere der Antwort von ShadowRanger , die auch erklärt, warum es so gemacht wird).

Demontage zeigt, dass BUILD_LIST_UNPACKverwendet wird:

>>> import dis
>>> dis.dis('[*a]')
  1           0 LOAD_NAME                0 (a)
              2 BUILD_LIST_UNPACK        1
              4 RETURN_VALUE

Das wird behandelt inceval.c , die eine leere Liste baut und erweitert sie (mit a):

        case TARGET(BUILD_LIST_UNPACK): {
            ...
            PyObject *sum = PyList_New(0);
              ...
                none_val = _PyList_Extend((PyListObject *)sum, PEEK(i));

_PyList_Extend verwendet list_extend :

_PyList_Extend(PyListObject *self, PyObject *iterable)
{
    return list_extend(self, iterable);
}

Was mit der Summe der Größen ruftlist_resize :

list_extend(PyListObject *self, PyObject *iterable)
    ...
        n = PySequence_Fast_GET_SIZE(iterable);
        ...
        m = Py_SIZE(self);
        ...
        if (list_resize(self, m + n) < 0) {

Und das lässt sich insgesamt wie folgt zusammenfassen:

list_resize(PyListObject *self, Py_ssize_t newsize)
{
  ...
    new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);

Lassen Sie uns das überprüfen. Berechnen Sie die erwartete Anzahl von Spots mit der obigen Formel und berechnen Sie die erwartete Bytegröße, indem Sie sie mit 8 multiplizieren (da ich hier 64-Bit-Python verwende) und die Bytegröße einer leeren Liste hinzufügen (dh den konstanten Overhead eines Listenobjekts). ::

from sys import getsizeof
for n in range(13):
    a = [None] * n
    expected_spots = n + (n >> 3) + (3 if n < 9 else 6)
    expected_bytesize = getsizeof([]) + expected_spots * 8
    real_bytesize = getsizeof([*a])
    print(n,
          expected_bytesize,
          real_bytesize,
          real_bytesize == expected_bytesize)

Ausgabe:

0 80 56 False
1 88 88 True
2 96 96 True
3 104 104 True
4 112 112 True
5 120 120 True
6 128 128 True
7 136 136 True
8 152 152 True
9 184 184 True
10 192 192 True
11 200 200 True
12 208 208 True

Spiele mit Ausnahme n = 0, die list_extendtatsächlich Verknüpfungen , so dass tatsächlich Streichhölzer auch:

        if (n == 0) {
            ...
            Py_RETURN_NONE;
        }
        ...
        if (list_resize(self, m + n) < 0) {
Stefan Pochmann
quelle
8

Dies sind Implementierungsdetails des CPython-Interpreters und daher möglicherweise nicht konsistent für andere Interpreter.

Das heißt, Sie können sehen, wo das Verständnis und list(a)Verhalten hier kommen:

https://github.com/python/cpython/blob/master/Objects/listobject.c#L36

Speziell für das Verständnis:

 * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
...

new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);

Direkt unter diesen Zeilen befindet sich list_preallocate_exactdie, die beim Anrufen verwendet wird list(a).

Geil
quelle
1
[*a]Es werden nicht einzelne Elemente einzeln angehängt. Es hat einen eigenen dedizierten Bytecode, über den die Masseneinfügung erfolgt extend.
ShadowRanger
Gotcha - Ich glaube, ich habe nicht weit genug darüber gegraben. Der Abschnitt über[*a]
Randy