Wie wird Pythons Liste implementiert?

181

Ist es eine verknüpfte Liste, ein Array? Ich suchte herum und fand nur Leute, die raten. Meine C-Kenntnisse sind nicht gut genug, um den Quellcode zu betrachten.

Greg
quelle

Antworten:

55

Es ist ein dynamisches Array . Praktischer Beweis: Die Indizierung dauert (natürlich mit extrem kleinen Unterschieden (0,0013 µs!)) Unabhängig vom Index dieselbe Zeit:

...>python -m timeit --setup="x = [None]*1000" "x[500]"
10000000 loops, best of 3: 0.0579 usec per loop

...>python -m timeit --setup="x = [None]*1000" "x[0]"
10000000 loops, best of 3: 0.0566 usec per loop

Ich wäre erstaunt, wenn IronPython oder Jython verknüpfte Listen verwenden würden - sie würden die Leistung vieler weit verbreiteter Bibliotheken ruinieren, die auf der Annahme beruhen, dass Listen dynamische Arrays sind.

user2357112 unterstützt Monica
quelle
1
@Ralf: Ich weiß, dass meine CPU (die meiste andere Hardware auch) alt und hundeschwach ist - auf der positiven Seite kann ich davon ausgehen, dass Code, der schnell genug für mich läuft, für alle Benutzer schnell genug ist: D
87
@delnan: -1 Dein "praktischer Beweis" ist ein Unsinn, genau wie die 6 positiven Stimmen. Etwa 98% der Zeit wird in Anspruch genommen x=[None]*1000, so dass die Messung möglicher Listenzugriffsunterschiede eher ungenau bleibt . Sie müssen die Initialisierung trennen:-s "x=[None]*100" "x[0]"
John Machin
26
Zeigt, dass es sich nicht um eine naive Implementierung einer verknüpften Liste handelt. Zeigt nicht definitiv, dass es sich um ein Array handelt.
Michael Mior
6
Sie können darüber hier lesen: docs.python.org/2/faq/design.html#how-are-lists-implemented
CCoder
3
Es gibt weit mehr Strukturen als nur verknüpfte Listen und Arrays. Das Timing ist für die Entscheidung zwischen ihnen praktisch nicht sinnvoll.
Ross Hemsley
235

Der C-Code ist eigentlich ziemlich einfach. Wenn Sie ein Makro erweitern und einige irrelevante Kommentare entfernen, befindet sich die Grundstruktur in listobject.h, die eine Liste definiert als:

typedef struct {
    PyObject_HEAD
    Py_ssize_t ob_size;

    /* Vector of pointers to list elements.  list[0] is ob_item[0], etc. */
    PyObject **ob_item;

    /* ob_item contains space for 'allocated' elements.  The number
     * currently in use is ob_size.
     * Invariants:
     *     0 <= ob_size <= allocated
     *     len(list) == ob_size
     *     ob_item == NULL implies ob_size == allocated == 0
     */
    Py_ssize_t allocated;
} PyListObject;

PyObject_HEADenthält einen Referenzzähler und eine Typkennung. Es ist also ein Vektor / Array, der insgesamt zugeordnet wird. Der Code zum Ändern der Größe eines solchen Arrays, wenn es voll ist, ist in listobject.c. Das Array wird nicht verdoppelt, sondern durch Zuweisung vergrößert

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

auf die Kapazität jedes Mal, wo newsizeist die angeforderte Größe (nicht unbedingt, allocated + 1weil Sie extenddurch eine beliebige Anzahl von Elementen können, anstatt appendsie einzeln zu geben).

Siehe auch die Python-FAQ .

Fred Foo
quelle
6
Wenn Sie also über Python-Listen iterieren, ist dies genauso langsam wie verknüpfte Listen, da jeder Eintrag nur ein Zeiger ist, sodass jedes Element höchstwahrscheinlich einen Cache-Fehler verursachen würde.
Kr0e
9
@ Kr0e: nicht wenn nachfolgende Elemente tatsächlich dasselbe Objekt sind :) Wenn Sie jedoch kleinere / Cache-freundlichere Datenstrukturen benötigen, sind das arrayModul oder NumPy vorzuziehen.
Fred Foo
@ Kr0e Ich würde nicht sagen, dass das Iterieren über die Liste so langsam ist wie verknüpfte Listen, aber das Iterieren über die Werte der verknüpften Listen ist langsam wie eine verknüpfte Liste, mit dem von Fred erwähnten Vorbehalt. Das Durchlaufen einer Liste zum Kopieren in eine andere Liste sollte beispielsweise schneller sein als eine verknüpfte Liste.
Ganea Dan Andrei
35

In CPython sind Listen Arrays von Zeigern. Andere Implementierungen von Python können sie auf unterschiedliche Weise speichern.

Bernstein
quelle
32

Dies ist implementierungsabhängig, aber IIRC:

  • CPython verwendet ein Array von Zeigern
  • Jython verwendet eine ArrayList
  • IronPython verwendet anscheinend auch ein Array. Sie können den Quellcode durchsuchen , um dies herauszufinden.

Somit haben alle O (1) Direktzugriff.

NullUserException
quelle
1
Implementierungsabhängig wie in einem Python-Interpreter, der Listen als verknüpfte Listen implementiert hat, wäre eine gültige Implementierung der Python-Sprache? Mit anderen Worten: O (1) Der zufällige Zugriff auf Listen ist nicht garantiert? Macht es das nicht unmöglich, effizienten Code zu schreiben, ohne sich auf Implementierungsdetails zu verlassen?
sepp2k
2
@sepp Ich glaube, Listen in Python sind nur geordnete Sammlungen. Die Implementierungs- und / oder Leistungsanforderungen dieser Implementierung werden nicht explizit angegeben
NullUserException
6
@ sppe2k: Da Python nicht wirklich eine Standard- oder formale Spezifikation hat (obwohl es einige Dokumentationen gibt, die sagen "... ist garantiert ..."), können Sie nicht 100% sicher sein, wie in "this" wird durch ein Stück Papier garantiert ". Da die O(1)Indizierung von Listen jedoch eine weit verbreitete und gültige Annahme ist, würde keine Implementierung es wagen, sie zu brechen.
@Paul Es sagt nichts darüber aus, wie die zugrunde liegende Implementierung von Listen erfolgen soll.
NullUserException
Es kommt einfach nicht vor, die große O-Laufzeit der Dinge anzugeben. Die Spezifikation der Sprachsyntax bedeutet nicht unbedingt dasselbe wie Implementierungsdetails, sondern ist häufig der Fall.
Paul McMillan
25

Ich würde Laurent Luces Artikel "Python List Implementation" vorschlagen . War für mich sehr nützlich, da der Autor erklärt, wie die Liste in CPython implementiert ist, und für diesen Zweck hervorragende Diagramme verwendet.

Listen Sie die Struktur des Objekts C auf

Ein Listenobjekt in CPython wird durch die folgende C-Struktur dargestellt. ob_itemist eine Liste von Zeigern auf die Listenelemente. zugewiesen ist die Anzahl der im Speicher zugewiesenen Steckplätze.

typedef struct {
    PyObject_VAR_HEAD
    PyObject **ob_item;
    Py_ssize_t allocated;
} PyListObject;

Es ist wichtig, den Unterschied zwischen zugewiesenen Slots und der Größe der Liste zu beachten. Die Größe einer Liste entspricht der len(l). Die Anzahl der zugewiesenen Steckplätze ist die im Speicher zugewiesene. Oft werden Sie feststellen, dass die zugewiesene Größe größer als die Größe sein kann. Auf diese Weise müssen Sie nicht reallocjedes Mal aufrufen, wenn ein neues Element an die Liste angehängt wird.

...

Anhängen

Wir fügen der Liste eine Ganzzahl hinzu : l.append(1). Was geschieht?
Geben Sie hier die Bildbeschreibung ein

Wir fahren fort, indem wir ein weiteres Element hinzufügen : l.append(2). list_resizewird mit n + 1 = 2 aufgerufen, aber da die zugewiesene Größe 4 ist, muss kein weiterer Speicher zugewiesen werden. Das gleiche passiert, wenn wir 2 weitere Ganzzahlen hinzufügen: l.append(3), l.append(4). Das folgende Diagramm zeigt, was wir bisher haben.

Geben Sie hier die Bildbeschreibung ein

...

Einfügen

Fügen wir an Position 1 eine neue Ganzzahl (5) ein: l.insert(1,5)und schauen wir uns an, was intern passiert.Geben Sie hier die Bildbeschreibung ein

...

Pop

Wenn Sie das letzte Element einfügen l.pop(), listpop()wird: aufgerufen. list_resizewird im Inneren aufgerufen listpop()und wenn die neue Größe weniger als die Hälfte der zugewiesenen Größe beträgt, wird die Liste verkleinert.Geben Sie hier die Bildbeschreibung ein

Sie können beobachten, dass Slot 4 immer noch auf die Ganzzahl zeigt, aber das Wichtigste ist die Größe der Liste, die jetzt 4 ist. Lassen Sie uns ein weiteres Element hinzufügen. In list_resize()ist Größe - 1 = 4 - 1 = 3 weniger als die Hälfte der zugewiesenen Slots, sodass die Liste auf 6 Slots verkleinert wird und die neue Größe der Liste jetzt 3 ist.

Sie können beobachten, dass Slot 3 und 4 immer noch auf einige Ganzzahlen verweisen, aber das Wichtigste ist die Größe der Liste, die jetzt 3 ist.Geben Sie hier die Bildbeschreibung ein

...

Python- Listenobjekt entfernen verfügt über eine Methode zum Entfernen eines bestimmten Elements : l.remove(5).Geben Sie hier die Bildbeschreibung ein

Lesya
quelle
Danke, ich verstehe den Link-Teil der Liste jetzt besser. Python-Liste ist eine aggregation, nicht composition. Ich wünschte, es gäbe auch eine Liste mit Kompositionen.
Shuva
22

Laut Dokumentation ,

Pythons Listen sind wirklich Arrays variabler Länge, keine verknüpften Listen im Lisp-Stil.

ravi77o
quelle
5

Wie andere oben angegeben haben, werden die Listen (wenn sie beträchtlich groß sind) implementiert, indem eine feste Menge an Speicherplatz zugewiesen wird und, falls dieser Speicherplatz gefüllt werden sollte, eine größere Menge an Speicherplatz zugewiesen und über die Elemente kopiert wird.

Um zu verstehen, warum die Methode ohne Verlust der Allgemeinheit O (1) amortisiert wird, nehmen wir an, dass wir a = 2 ^ n Elemente eingefügt haben und nun unsere Tabelle auf 2 ^ (n + 1) Größe verdoppeln müssen. Das heißt, wir führen derzeit 2 ^ (n + 1) Operationen aus. Letzte Kopie, wir haben 2 ^ n Operationen durchgeführt. Vorher haben wir 2 ^ (n-1) gemacht ... bis hinunter zu 8,4,2,1. Wenn wir diese addieren, erhalten wir 1 + 2 + 4 + 8 + ... + 2 ^ (n + 1) = 2 ^ (n + 2) - 1 <4 * 2 ^ n = O (2 ^ n) = O (a) Gesamtinsertionen (dh O (1) Amortisationszeit). Es sollte auch beachtet werden, dass, wenn die Tabelle Löschungen zulässt, das Schrumpfen der Tabelle mit einem anderen Faktor erfolgen muss (z. B. 3x).

RussellStewart
quelle
Soweit ich weiß, werden ältere Elemente nicht kopiert. Es wird mehr Speicherplatz zugewiesen, aber der neue Speicherplatz grenzt nicht an den bereits genutzten Speicherplatz an, und nur die neueren Elemente, die eingefügt werden sollen, werden in den neuen Speicherplatz kopiert. Bitte korrigieren Sie mich, wenn ich falsch liege.
Tushar Vazirani
1

Eine Liste in Python ist so etwas wie ein Array, in dem Sie mehrere Werte speichern können. Die Liste ist veränderbar, dh Sie können sie ändern. Das Wichtigste, was Sie wissen sollten, wenn wir eine Liste erstellen, erstellt Python automatisch eine Referenz-ID für diese Listenvariable. Wenn Sie es ändern, indem Sie eine andere Variable zuweisen, wird die Hauptliste geändert. Versuchen wir es mit einem Beispiel:

list_one = [1,2,3,4]

my_list = list_one

#my_list: [1,2,3,4]

my_list.append("new")

#my_list: [1,2,3,4,'new']
#list_one: [1,2,3,4,'new']

Wir fügen hinzu, my_listaber unsere Hauptliste hat sich geändert. Die Liste dieses Mittelwerts wurde nicht als Kopierliste zugewiesen, die als Referenz zugewiesen wurde.

hasib
quelle
0

In CPython wird die Liste als dynamisches Array implementiert. Wenn wir zu diesem Zeitpunkt anhängen, wird nicht nur ein Makro hinzugefügt, sondern es wird etwas mehr Speicherplatz zugewiesen, sodass nicht jedes Mal neuer Speicherplatz hinzugefügt werden sollte.

gaurav
quelle