Ich verstehe nicht, wie das Durchlaufen eines Wörterbuchs oder eines Satzes in Python in 'beliebiger' Reihenfolge erfolgt.
Ich meine, es ist eine Programmiersprache, also muss alles in der Sprache zu 100% bestimmt sein, richtig? Python muss über einen Algorithmus verfügen, der entscheidet, welcher Teil des Wörterbuchs oder der Menge ausgewählt wird, 1., 2. usw.
Was vermisse ich?
python
dictionary
set
python-internals
Edgar Aroutiounian
quelle
quelle
Antworten:
Die Reihenfolge ist nicht willkürlich, sondern hängt vom Einfüge- und Löschverlauf des Wörterbuchs oder Satzes sowie von der spezifischen Python-Implementierung ab. Für den Rest dieser Antwort können Sie für 'Wörterbuch' auch 'set' lesen. Mengen werden als Wörterbücher mit nur Schlüsseln und ohne Werte implementiert.
Schlüssel werden gehasht und Hash-Werte werden Slots in einer dynamischen Tabelle zugewiesen (sie können je nach Bedarf wachsen oder schrumpfen). Und dieser Zuordnungsprozess kann zu Kollisionen führen, was bedeutet, dass ein Schlüssel in einen nächsten Steckplatz gesteckt werden muss, basierend auf dem, was bereits vorhanden ist.
Das Auflisten der Inhaltsschleifen über den Steckplätzen führt dazu, dass die Schlüssel in der Reihenfolge aufgelistet werden, in der sie sich derzeit in der Tabelle befinden.
Nehmen Sie zum Beispiel die Schlüssel
'foo'
und'bar'
und nehmen wir an, dass die Tabellengröße 8 Steckplätze beträgt. In Python 2.7hash('foo')
ist-4177197833195190597
,hash('bar')
ist327024216814240868
. Modulo 8, dh diese beiden Tasten sind in die Steckplätze 3 und 4 eingesteckt.Dies informiert ihre Auflistungsreihenfolge:
Alle Steckplätze außer 3 und 4 sind leer. In einer Schleife über die Tabelle werden zuerst Steckplatz 3 und dann Steckplatz 4
'foo'
aufgelistet'bar'
.bar
undbaz
haben jedoch Hash-Werte, die genau 8 voneinander entfernt sind und somit genau demselben Slot zugeordnet sind4
:Ihre Reihenfolge hängt nun davon ab, welcher Schlüssel zuerst gesteckt wurde. Der zweite Schlüssel muss in einen nächsten Steckplatz verschoben werden:
Die Tabellenreihenfolge unterscheidet sich hier, da der eine oder andere Schlüssel zuerst gesteckt wurde.
Der technische Name für die zugrunde liegende Struktur, die von CPython verwendet wird (die am häufigsten verwendete Python-Implementierung), ist eine Hash-Tabelle , die eine offene Adressierung verwendet. Wenn Sie neugierig sind und C gut genug verstehen, werfen Sie einen Blick auf die C-Implementierung, um alle (gut dokumentierten) Details zu erhalten. Sie können sich auch diese Pycon 2010-Präsentation von Brandon Rhodes über die Funktionsweise von CPython ansehen
dict
oder eine Kopie von Beautiful Code abholen , die ein Kapitel über die von Andrew Kuchling geschriebene Implementierung enthält.Beachten Sie, dass ab Python 3.3 auch ein zufälliger Hash-Startwert verwendet wird, wodurch Hash-Kollisionen unvorhersehbar werden, um bestimmte Arten von Denial-of-Service zu verhindern (wenn ein Angreifer einen Python-Server durch Massen-Hash-Kollisionen nicht mehr reagiert). Dies bedeutet, dass die Reihenfolge eines bestimmten Wörterbuchs oder Satzes dann auch vom zufälligen Hash-Startwert für den aktuellen Python-Aufruf abhängt.
Andere Implementierungen können eine andere Struktur für Wörterbücher verwenden, sofern sie die für sie dokumentierte Python-Schnittstelle erfüllen. Ich glaube jedoch, dass alle Implementierungen bisher eine Variation der Hash-Tabelle verwenden.
CPython 3.6 führt eine neue
dict
Implementierung ein, die die Einfügereihenfolge beibehält und schneller und speichereffizienter zu starten ist. Anstatt eine große Tabelle mit geringer Dichte zu führen, in der jede Zeile auf den gespeicherten Hashwert sowie auf die Schlüssel- und Wertobjekte verweist, fügt die neue Implementierung ein kleineres Hash- Array hinzu , das nur auf Indizes in einer separaten 'dichten' Tabelle verweist (eine, die nur so viele Zeilen enthält da es tatsächliche Schlüssel-Wert-Paare gibt), und es ist die dichte Tabelle, die die enthaltenen Elemente der Reihe nach auflistet. Weitere Informationen finden Sie im Vorschlag an Python-Dev . Beachten Sie, dass dies in Python 3.6 als Implementierungsdetail betrachtet wirdPython-the-language gibt nicht an, dass andere Implementierungen die Reihenfolge beibehalten müssen. Dies änderte sich in Python 3.7, wo dieses Detail zu einer Sprachspezifikation erhoben wurde . Damit eine Implementierung ordnungsgemäß mit Python 3.7 oder neuer kompatibel ist, muss dieses auftragserhaltende Verhalten kopiert werden. Und um es klar auszudrücken: Diese Änderung gilt nicht für Mengen, da Mengen bereits eine 'kleine' Hash-Struktur haben.Python 2.7 und höher bietet auch eine
OrderedDict
Klasse , deren Unterklassedict
eine zusätzliche Datenstruktur zum Aufzeichnen der Schlüsselreihenfolge hinzufügt. Diese Klasse merkt sich zum Preis von Geschwindigkeit und zusätzlichem Speicher, in welcher Reihenfolge Sie Schlüssel eingefügt haben. Das Auflisten von Schlüsseln, Werten oder Elementen erfolgt dann in dieser Reihenfolge. Es verwendet eine doppelt verknüpfte Liste, die in einem zusätzlichen Wörterbuch gespeichert ist, um die Bestellung effizient auf dem neuesten Stand zu halten. Siehe den Beitrag von Raymond Hettinger, der die Idee umreißt .OrderedDict
Objekte haben andere Vorteile, wie zum Beispiel die Nachbestellbarkeit .Wenn Sie ein bestelltes Set wünschen, können Sie das
oset
Paket installieren . Es funktioniert unter Python 2.5 und höher.quelle
__hash__
und__eq__
(und nichts anderes) verwenden, ist praktisch eine Sprachgarantie, kein Implementierungsdetail.dictobject.c
) und am Ende weitaus weniger Vergleiche zu erzielen, als ein BTree benötigt, um überhaupt das richtige zu finden Teilbaum.Dies ist eher eine Antwort auf Python 3.41 Ein Satz, bevor er als Duplikat geschlossen wurde.
Die anderen haben Recht: Verlassen Sie sich nicht auf die Bestellung. Tu nicht einmal so, als gäbe es einen.
Es gibt jedoch eine Sache, auf die Sie sich verlassen können:
Das heißt, die Reihenfolge ist stabil .
Um zu verstehen, warum es eine wahrgenommene Ordnung gibt, müssen einige Dinge verstanden werden:
Dass Python Hash-Sets verwendet ,
Wie CPythons Hash-Set im Speicher gespeichert wird und
Wie Zahlen gehasht werden
Von oben:
Ein Hash-Set ist eine Methode zum Speichern von Zufallsdaten mit sehr schnellen Suchzeiten.
Es hat ein Hintergrundarray:
Wir werden das spezielle Dummy-Objekt ignorieren, das nur existiert, um das Behandeln von Entfernungen zu vereinfachen, da wir nicht aus diesen Mengen entfernen werden.
Um wirklich schnell nachschlagen zu können, zaubern Sie einen Hash aus einem Objekt. Die einzige Regel ist, dass zwei Objekte, die gleich sind, denselben Hash haben. (Wenn jedoch zwei Objekte denselben Hash haben, können sie ungleich sein.)
Sie erstellen dann einen Index, indem Sie den Modul anhand der Arraylänge nehmen:
Dies macht es sehr schnell, auf Elemente zuzugreifen.
Hashes sind nur die meisten der Geschichte, wie
hash(n) % len(storage)
undhash(m) % len(storage)
in gleicher Anzahl zur Folge haben kann. In diesem Fall können verschiedene Strategien versuchen, den Konflikt zu lösen. CPython verwendet 9-mal "lineares Prüfen", bevor komplizierte Dinge ausgeführt werden. Daher wird links vom Slot nach bis zu 9 Stellen gesucht, bevor nach einer anderen Stelle gesucht wird.CPythons Hash-Sets werden wie folgt gespeichert:
Ein Hash-Set darf nicht mehr als 2/3 voll sein . Wenn 20 Elemente vorhanden sind und das Hintergrundarray 30 Elemente lang ist, wird die Größe des Hintergrundspeichers größer. Dies liegt daran, dass Kollisionen mit kleinen Backing-Stores häufiger auftreten und Kollisionen alles verlangsamen.
Die Größe des Hintergrundspeichers wird in Potenzen von 4 beginnend bei 8 geändert, mit Ausnahme von großen Sätzen (50.000 Elemente), deren Größe in Zweierpotenzen geändert wird: (8, 32, 128, ...).
Wenn Sie also ein Array erstellen, hat der Hintergrundspeicher die Länge 8. Wenn er 5 voll ist und Sie ein Element hinzufügen, enthält er kurz 6 Elemente.
6 > ²⁄₃·8
Dies löst also eine Größenänderung aus, und der Hintergrundspeicher vervierfacht sich auf Größe 32.Schließlich wird
hash(n)
nurn
für Zahlen zurückgegeben (außer-1
was speziell ist).Schauen wir uns also den ersten an:
len(v_set)
ist 10, also ist der Hintergrundspeicher mindestens 15 (+1), nachdem alle Elemente hinzugefügt wurden . Die relevante Potenz von 2 ist 32. Der Hintergrundspeicher lautet also:Wir haben
also diese einfügen als:
Wir würden also eine Bestellung wie erwarten
mit der 1 oder 33 ist das nicht woanders am Start. Dies wird eine lineare Abtastung verwenden, also haben wir entweder:
oder
Sie können erwarten, dass die 33 diejenige ist, die verschoben wurde, weil die 1 bereits vorhanden war. Aufgrund der Größenänderung, die beim Erstellen des Sets auftritt, ist dies jedoch nicht der Fall. Jedes Mal, wenn das Set neu erstellt wird, werden die bereits hinzugefügten Elemente effektiv neu angeordnet.
Jetzt können Sie sehen warum
könnte in Ordnung sein. Es gibt 14 Elemente, sodass der Hintergrundspeicher mindestens 21 + 1 beträgt, was 32 bedeutet:
1 bis 13 Hash in den ersten 13 Slots. 20 geht in Steckplatz 20.
55 geht in Slot,
hash(55) % 32
der 23 ist:Wenn wir stattdessen 50 wählen würden, würden wir erwarten
Und siehe da:
pop
wird ganz einfach durch das Aussehen der Dinge implementiert: Es durchläuft die Liste und öffnet die erste.Dies ist alles Implementierungsdetail.
quelle
"Willkürlich" ist nicht dasselbe wie "unbestimmt".
Was sie sagen ist, dass es keine nützlichen Eigenschaften der Wörterbuch-Iterationsreihenfolge gibt, die "in der öffentlichen Schnittstelle" sind. Es gibt mit ziemlicher Sicherheit viele Eigenschaften der Iterationsreihenfolge, die vollständig durch den Code bestimmt werden, der derzeit die Wörterbuchiteration implementiert, aber die Autoren versprechen Ihnen diese nicht als etwas, das Sie verwenden können. Dies gibt ihnen mehr Freiheit, diese Eigenschaften zwischen Python-Versionen zu ändern (oder sogar nur unter verschiedenen Betriebsbedingungen oder zur Laufzeit völlig zufällig), ohne befürchten zu müssen, dass Ihr Programm kaputt geht.
Wenn Sie also ein Programm schreiben, das von einer Eigenschaft in der gesamten Wörterbuchreihenfolge abhängt , brechen Sie den Vertrag über die Verwendung des Wörterbuchtyps, und die Python-Entwickler versprechen nicht, dass dies immer funktioniert, auch wenn es zu funktionieren scheint für jetzt, wenn Sie es testen. Es ist im Grunde das Äquivalent zu "undefiniertem Verhalten" in C.
quelle
d.items()
ist im Wesentlichen identisch mitzip(d.keys(), d.values())
. Wenn jedoch Elemente zum Wörterbuch hinzugefügt werden, sind alle Wetten ungültig. Die Reihenfolge kann sich vollständig ändern (wenn die Größe der Hash-Tabelle geändert werden muss), obwohl das neue Element die meiste Zeit nur an einer beliebigen Stelle in der Sequenz auftaucht.Die anderen Antworten auf diese Frage sind ausgezeichnet und gut geschrieben. Das OP fragt "wie", was ich als "wie kommen sie davon" oder "warum" interpretiere.
In der Python-Dokumentation heißt es, dass Wörterbücher nicht geordnet sind, da das Python-Wörterbuch das assoziative Array des abstrakten Datentyps implementiert . Wie sie sagen
Mit anderen Worten, ein Informatikstudent kann nicht davon ausgehen, dass ein assoziatives Array geordnet ist. Gleiches gilt für Mengen in Mathematik
und Informatik
Das Implementieren eines Wörterbuchs mithilfe einer Hash-Tabelle ist ein Implementierungsdetail , das insofern interessant ist, als es hinsichtlich der Reihenfolge dieselben Eigenschaften wie assoziative Arrays aufweist.
quelle
Python verwendet eine Hash-Tabelle zum Speichern der Wörterbücher, daher gibt es keine Reihenfolge in Wörterbüchern oder anderen iterierbaren Objekten, die eine Hash-Tabelle verwenden.
Aber in Bezug auf die Indizes der Elemente in einem Hash - Objekt, berechnen Python die Indizes basieren auf folgenden Code in
hashtable.c
:Da der Hash-Wert von Ganzzahlen die Ganzzahl selbst ist *, basiert der Index auf der Zahl (
ht->num_buckets - 1
ist eine Konstante), so dass der von Bitwise-and zwischen(ht->num_buckets - 1)
und der Zahl selbst berechnete Index * (erwarten Sie für -1, dass sein Hash -2 ist ) und für andere Objekte mit ihrem Hashwert.Betrachten Sie das folgende Beispiel mit
set
dieser Hash-Tabelle:Für die Nummer haben
33
wir:Das ist eigentlich:
Hinweis in diesem Fall
(ht->num_buckets - 1)
ist8-1=7
oder0b111
.Und für
1919
:Und für
333
:Weitere Informationen zur Python-Hash-Funktion finden Sie in den folgenden Zitaten aus dem Python-Quellcode :
* Die Hash-Funktion für die Klasse
int
:quelle
Ab Python 3.7 (und bereits in CPython 3.6 ) bleiben die Wörterbuchelemente in der Reihenfolge, in der sie eingefügt wurden .
quelle