Was ist ein korrekter und guter Weg, um zu implementieren __hash__()
?
Ich spreche von der Funktion, die einen Hashcode zurückgibt, der dann zum Einfügen von Objekten in Hashtabellen oder Wörterbücher verwendet wird.
Da __hash__()
eine Ganzzahl zurückgegeben wird und zum "Binning" von Objekten in Hashtabellen verwendet wird, gehe ich davon aus, dass die Werte der zurückgegebenen Ganzzahl für allgemeine Daten gleichmäßig verteilt sein sollten (um Kollisionen zu minimieren). Was ist eine gute Praxis, um solche Werte zu erhalten? Sind Kollisionen ein Problem? In meinem Fall habe ich eine kleine Klasse, die als Containerklasse fungiert und einige Ints, einige Floats und eine Zeichenfolge enthält.
quelle
__key
Funktion ist dies ungefähr so schnell wie jeder Hash sein kann. Sicher, wenn bekannt ist, dass die Attribute Ganzzahlen sind und es nicht zu viele davon gibt, könnten Sie mit einem selbst gerollten Hash möglicherweise etwas schneller laufen , aber es wäre wahrscheinlich nicht so gut verteilt.hash((self.attr_a, self.attr_b, self.attr_c))
wird überraschend schnell (und korrekt ) sein, da die Erstellung kleinertuple
s speziell optimiert ist und die Arbeit des Abrufens und Kombinierens von Hashes zu C-Builtins vorantreibt, was normalerweise schneller als Python-Code ist.None
sobald sich der Schlüssel ändert. Ich habe es gelöst, indem ich die ID des Objekts als Schlüssel anstatt nur als Objekt gespeichert habe.John Millikin schlug eine ähnliche Lösung vor:
Das Problem bei dieser Lösung ist, dass die
hash(A(a, b, c)) == hash((a, b, c))
. Mit anderen Worten, der Hash kollidiert mit dem des Tupels seiner Schlüsselmitglieder. Vielleicht spielt das in der Praxis keine Rolle?Update: In den Python-Dokumenten wird jetzt empfohlen, ein Tupel wie im obigen Beispiel zu verwenden. Beachten Sie, dass in der Dokumentation angegeben ist
Beachten Sie, dass das Gegenteil nicht der Fall ist. Objekte, die nicht gleich sind, haben möglicherweise denselben Hashwert. Eine solche Hash-Kollision führt nicht dazu, dass ein Objekt ein anderes ersetzt, wenn es als Diktatschlüssel oder Set-Element verwendet wird , solange die Objekte nicht auch gleich sind .
Veraltete / schlechte Lösung
Die Python-Dokumentation zu, was uns__hash__
schlägt vor, die Hashes der Unterkomponenten mit etwas wie XOR zu kombinierenFolgendesgibt:Update: Wie Blckknght betont, kann das Ändern der Reihenfolge von a, b und c zu Problemen führen. Ich habe eine zusätzliche hinzugefügt
^ hash((self._a, self._b, self._c))
, um die Reihenfolge der gehashten Werte zu erfassen. Dieses Finale^ hash(...)
kann entfernt werden, wenn die zu kombinierenden Werte nicht neu angeordnet werden können (z. B. wenn sie unterschiedliche Typen haben und daher der Wert von_a
niemals_b
oder_c
usw. zugewiesen wird).quelle
hash(A(1, 2, 3))
wird gleichhash(A(3, 1, 2))
(und sie werden beide Hash gleich jede andereA
Instanz mit einer Permutation1
,2
und3
als seine Werte). Wenn Sie vermeiden möchten, dass Ihre Instanz denselben Hash wie ein Tupel ihrer Argumente hat, erstellen Sie einfach einen Sentinel-Wert (entweder als Klassenvariable oder als global) und fügen Sie ihn dann in das zu hashende Tupel ein: return hash ((_ sentinel) , self._a, self._b, self._c))isinstance
könnte problematisch sein, da ein Objekt einer Unterklasse vontype(self)
jetzt gleich einem Objekt von sein kanntype(self)
. Sie können also feststellen, dass das Hinzufügen von aCar
und aFord
zu aset()
je nach Einfügereihenfolge nur zu einem eingefügten Objekt führen kann. Darüber hinaus kann es vorkommen, dass Sie aufa == b
True, aber aufb == a
False stoßen.B
, können Sie dies inisinstance(othr, B)
hash((type(self), self._a, self._b, self._c))
.B
anstelle von verwendet wirdtype(self)
, wird es oft als bessere Vorgehensweise angesehen, zurückzukehren,NotImplemented
wenn ein unerwarteter Typ__eq__
anstelle von verwendet wirdFalse
. Auf diese Weise können andere benutzerdefinierte Typen einen Typ implementieren__eq__
, der über diesen Typ Bescheid weißB
und ihn vergleichen kann, wenn sie dies wünschen.Paul Larson von Microsoft Research untersuchte eine Vielzahl von Hash-Funktionen. Er hat mir das erzählt
funktionierte überraschend gut für eine Vielzahl von Saiten. Ich habe festgestellt, dass ähnliche Polynomtechniken gut für die Berechnung eines Hashs unterschiedlicher Unterfelder geeignet sind.
quelle
(hash * 1000003) XOR ord(c)
für Zeichenfolgen mit 32-Bit-Wraparound-Multiplikation. [Zitat ]__hash__
Methode bereitstellen . Wir müssen nicht unsere eigenen rollen. Die Frage ist, wie__hash__
eine typische benutzerdefinierte Klasse implementiert werden soll (mit einer Reihe von Eigenschaften, die auf integrierte Typen oder möglicherweise auf andere benutzerdefinierte Klassen verweisen), die in dieser Antwort überhaupt nicht behandelt werden.Ich kann versuchen, den zweiten Teil Ihrer Frage zu beantworten.
Die Kollisionen resultieren wahrscheinlich nicht aus dem Hash-Code selbst, sondern aus der Zuordnung des Hash-Codes zu einem Index in einer Sammlung. So könnte Ihre Hash-Funktion beispielsweise zufällige Werte von 1 bis 10000 zurückgeben. Wenn Ihre Hash-Tabelle jedoch nur 32 Einträge enthält, werden beim Einfügen Kollisionen auftreten.
Darüber hinaus würde ich denken, dass Kollisionen von der Sammlung intern aufgelöst werden, und es gibt viele Methoden, um Kollisionen aufzulösen. Das einfachste (und schlechteste) ist, wenn Sie einen Eintrag zum Einfügen am Index i erhalten, 1 zu i addieren, bis Sie eine leere Stelle finden und dort einfügen. Das Abrufen funktioniert dann genauso. Dies führt zu ineffizienten Abrufen für einige Einträge, da Sie möglicherweise einen Eintrag haben, für dessen Suche die gesamte Sammlung durchlaufen werden muss!
Andere Kollisionsauflösungsmethoden reduzieren die Abrufzeit, indem Einträge in der Hash-Tabelle verschoben werden, wenn ein Element eingefügt wird, um Dinge zu verteilen. Dies erhöht die Einfügezeit, setzt jedoch voraus, dass Sie mehr lesen als einfügen. Es gibt auch Methoden, die versuchen, verschiedene kollidierende Einträge zu verzweigen, damit Einträge an einer bestimmten Stelle gruppiert werden.
Wenn Sie die Größe der Sammlung ändern müssen, müssen Sie alles erneut aufbereiten oder eine dynamische Hashing-Methode verwenden.
Kurz gesagt, je nachdem, wofür Sie den Hash-Code verwenden, müssen Sie möglicherweise Ihre eigene Kollisionsauflösungsmethode implementieren. Wenn Sie sie nicht in einer Sammlung speichern, können Sie wahrscheinlich mit einer Hash-Funktion davonkommen, die nur Hash-Codes in einem sehr großen Bereich generiert. In diesem Fall können Sie sicherstellen, dass Ihr Container größer ist als er sein muss (je größer, desto besser natürlich), abhängig von Ihren Speicherproblemen.
Hier sind einige Links, wenn Sie mehr interessiert sind:
Koalesziertes Hashing auf Wikipedia
Wikipedia hat auch eine Zusammenfassung verschiedener Methoden zur Kollisionsauflösung:
Außerdem behandelt " File Organization And Processing " von Tharp viele Methoden zur Kollisionsauflösung ausführlich. IMO ist eine großartige Referenz für Hashing-Algorithmen.
quelle
Eine sehr gute Erklärung, wann und wie die
__hash__
Funktion implementiert wird, finden Sie auf der Website von programiz :Nur ein Screenshot, um einen Überblick zu geben: (Abgerufen am 13.12.2019)
Für eine persönliche Implementierung der Methode bietet die oben genannte Site ein Beispiel, das der Antwort von millerdev entspricht .
quelle
Hängt von der Größe des zurückgegebenen Hashwerts ab. Es ist eine einfache Logik, dass Sie Kollisionen bekommen, wenn Sie einen 32-Bit-Int basierend auf dem Hash von vier 32-Bit-Ints zurückgeben müssen.
Ich würde Bitoperationen bevorzugen. Wie der folgende C-Pseudocode:
Ein solches System könnte auch für Floats funktionieren, wenn Sie sie einfach als Bitwert verwenden, anstatt tatsächlich einen Gleitkommawert darzustellen, vielleicht besser.
Für Streicher habe ich wenig / keine Ahnung.
quelle