Was ist ein korrekter und guter Weg, um __hash __ () zu implementieren?

150

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.

user229898
quelle

Antworten:

185

Eine einfache und korrekte Implementierung __hash__()ist die Verwendung eines Schlüsseltupels. Es ist nicht so schnell wie ein spezialisierter Hash, aber wenn Sie das brauchen, sollten Sie den Typ wahrscheinlich in C implementieren.

Hier ist ein Beispiel für die Verwendung eines Schlüssels für Hash und Gleichheit:

class A:
    def __key(self):
        return (self.attr_a, self.attr_b, self.attr_c)

    def __hash__(self):
        return hash(self.__key())

    def __eq__(self, other):
        if isinstance(other, A):
            return self.__key() == other.__key()
        return NotImplemented

Außerdem enthält die Dokumentation von__hash__ mehr Informationen, die unter bestimmten Umständen wertvoll sein können.

John Millikin
quelle
1
Abgesehen von dem geringen Aufwand durch das Ausklammern der __keyFunktion 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 kleiner tuples speziell optimiert ist und die Arbeit des Abrufens und Kombinierens von Hashes zu C-Builtins vorantreibt, was normalerweise schneller als Python-Code ist.
ShadowRanger
Angenommen, ein Objekt der Klasse A wird als Schlüssel für ein Wörterbuch verwendet. Wenn sich ein Attribut der Klasse A ändert, ändert sich auch sein Hashwert. Würde das nicht ein Problem schaffen?
Herr Matrix
1
Wie in der folgenden Antwort von @oved.by.Jesus erwähnt, sollte die Hash-Methode für ein veränderliches Objekt nicht definiert / überschrieben werden (standardmäßig definiert und verwendet ID für Gleichheit und Vergleich).
Herr Matrix
@ Miguel, ich bin auf das genaue Problem gestoßen. Was passiert, ist, dass das Wörterbuch zurückkehrt, Nonesobald 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.
Jaswant P
@JaswantP Python verwendet standardmäßig die ID des Objekts als Schlüssel für jedes hashbare Objekt.
Herr Matrix
22

John Millikin schlug eine ähnliche Lösung vor:

class A(object):

    def __init__(self, a, b, c):
        self._a = a
        self._b = b
        self._c = c

    def __eq__(self, othr):
        return (isinstance(othr, type(self))
                and (self._a, self._b, self._c) ==
                    (othr._a, othr._b, othr._c))

    def __hash__(self):
        return hash((self._a, self._b, self._c))

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

Die einzige erforderliche Eigenschaft ist, dass Objekte, die gleich sind, denselben Hashwert haben

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__hash__ schlägt vor, die Hashes der Unterkomponenten mit etwas wie XOR zu kombinieren , was uns Folgendes gibt:

class B(object):

    def __init__(self, a, b, c):
        self._a = a
        self._b = b
        self._c = c

    def __eq__(self, othr):
        if isinstance(othr, type(self)):
            return ((self._a, self._b, self._c) ==
                    (othr._a, othr._b, othr._c))
        return NotImplemented

    def __hash__(self):
        return (hash(self._a) ^ hash(self._b) ^ hash(self._c) ^
                hash((self._a, self._b, self._c)))

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 _aniemals _boder _cusw. zugewiesen wird).

millerdev
quelle
5
Normalerweise möchten Sie die Attribute nicht direkt XOR-verknüpfen, da dies zu Kollisionen führt, wenn Sie die Reihenfolge der Werte ändern. Das heißt, hash(A(1, 2, 3))wird gleich hash(A(3, 1, 2))(und sie werden beide Hash gleich jede andere AInstanz mit einer Permutation 1, 2und 3als 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))
Blckknght
1
Ihre Verwendung von isinstancekönnte problematisch sein, da ein Objekt einer Unterklasse von type(self)jetzt gleich einem Objekt von sein kann type(self). Sie können also feststellen, dass das Hinzufügen von a Carund a Fordzu a set()je nach Einfügereihenfolge nur zu einem eingefügten Objekt führen kann. Darüber hinaus kann es vorkommen, dass Sie auf a == bTrue, aber auf b == aFalse stoßen.
MaratC
1
Wenn Sie eine Unterklasse haben B, können Sie dies inisinstance(othr, B)
millerdev
7
Ein Gedanke: Das Schlüsseltupel könnte den Klassentyp enthalten, wodurch verhindert wird, dass andere Klassen mit demselben Schlüsselsatz von Attributen als gleich angezeigt werden : hash((type(self), self._a, self._b, self._c)).
Ben Mosher
2
Neben dem Punkt, dass Banstelle von verwendet wird type(self), wird es oft als bessere Vorgehensweise angesehen, zurückzukehren, NotImplementedwenn ein unerwarteter Typ __eq__anstelle von verwendet wird False. Auf diese Weise können andere benutzerdefinierte Typen einen Typ implementieren __eq__, der über diesen Typ Bescheid weiß Bund ihn vergleichen kann, wenn sie dies wünschen.
Mark Amery
16

Paul Larson von Microsoft Research untersuchte eine Vielzahl von Hash-Funktionen. Er hat mir das erzählt

for c in some_string:
    hash = 101 * hash  +  ord(c)

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.

George V. Reilly
quelle
8
Anscheinend macht Java es genauso, aber mit 31 statt 101
user229898
3
Was ist der Grund für die Verwendung dieser Zahlen? Gibt es einen Grund, 101 oder 31 zu wählen?
Bigblind
1
Hier ist eine Erklärung für Primzahlmultiplikatoren: stackoverflow.com/questions/3613102/… . 101 scheint besonders gut zu funktionieren, basierend auf Paul Larsons Experimenten.
George V. Reilly
4
Python verwendet (hash * 1000003) XOR ord(c)für Zeichenfolgen mit 32-Bit-Wraparound-Multiplikation. [Zitat ]
Tyler
4
Selbst wenn dies zutrifft, ist dies in diesem Zusammenhang nicht von praktischem Nutzen, da die integrierten Python-Zeichenfolgentypen bereits eine __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.
Mark Amery
3

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.

Kryptisch
quelle
1

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)

Screenshot von https://www.programiz.com/python-programming/methods/built-in/hash 2019-12-13

Für eine persönliche Implementierung der Methode bietet die oben genannte Site ein Beispiel, das der Antwort von millerdev entspricht .

class Person:
def __init__(self, age, name):
    self.age = age
    self.name = name

def __eq__(self, other):
    return self.age == other.age and self.name == other.name

def __hash__(self):
    print('The hash is:')
    return hash((self.age, self.name))

person = Person(23, 'Adam')
print(hash(person))
geliebt von Jesus
quelle
0

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:

int a;
int b;
int c;
int d;
int hash = (a & 0xF000F000) | (b & 0x0F000F00) | (c & 0x00F000F0 | (d & 0x000F000F);

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.

Hündchen
quelle
Ich weiß, dass es zu Kollisionen kommen wird. Aber ich habe keine Ahnung, wie damit umgegangen wird. Außerdem sind meine Attributwerte in Kombination sehr spärlich verteilt, sodass ich nach einer intelligenten Lösung suchte. Und irgendwie habe ich erwartet, dass es irgendwo eine Best Practice gibt.
user229898