Objekt vom benutzerdefinierten Typ als Wörterbuchschlüssel

185

Was muss ich tun, um meine Objekte eines benutzerdefinierten Typs als Schlüssel in einem Python-Wörterbuch zu verwenden (wobei ich nicht möchte, dass die "Objekt-ID" als Schlüssel fungiert), z

class MyThing:
    def __init__(self,name,location,length):
            self.name = name
            self.location = location
            self.length = length

Ich möchte MyThing's als Schlüssel verwenden, die als gleich angesehen werden, wenn Name und Ort gleich sind. Von C # / Java bin ich es gewohnt, eine Methode für Gleichheit und Hashcode überschreiben und bereitstellen zu müssen, und verspreche, nichts zu mutieren, von dem der Hashcode abhängt.

Was muss ich in Python tun, um dies zu erreichen? Soll ich überhaupt?

(In einem einfachen Fall, wie hier, ist es vielleicht besser, einfach ein Tupel (Name, Ort) als Schlüssel zu platzieren - aber ich möchte, dass der Schlüssel ein Objekt ist.)

Anonym
quelle
Was ist falsch an der Verwendung des Hash?
Rafe Kettler
5
Wahrscheinlich, weil er möchte, dass zwei MyThing, wenn sie dasselbe haben nameund locationdas Wörterbuch indizieren, um denselben Wert zurückzugeben, auch wenn sie separat als zwei verschiedene "Objekte" erstellt wurden.
Santa
1
"Vielleicht ist es besser, nur ein (Name, Ort) Tupel als Schlüssel zu platzieren - aber denken Sie daran, ich möchte, dass der Schlüssel ein Objekt ist.)" Sie meinen: ein NICHT VERBUNDENES Objekt?
Eyquem

Antworten:

219

Sie müssen 2 Methoden hinzufügen , beachten Sie __hash__und __eq__:

class MyThing:
    def __init__(self,name,location,length):
        self.name = name
        self.location = location
        self.length = length

    def __hash__(self):
        return hash((self.name, self.location))

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

    def __ne__(self, other):
        # Not strictly necessary, but to avoid having both x==y and x!=y
        # True at the same time
        return not(self == other)

In der Python- Diktatdokumentation werden diese Anforderungen für Schlüsselobjekte definiert, dh sie müssen hashbar sein .

6502
quelle
17
hash(self.name)sieht besser aus als self.name.__hash__(), und wenn Sie dies tun und tun können hash((x, y)), um XORing selbst zu vermeiden.
Rosh Oxymoron
5
Als zusätzliche Anmerkung, ich , dass gerade entdeckt , ruft x.__hash__()wie das auch ist falsch , weil es kann produzieren falsche Ergebnisse: pastebin.com/C9fSH7eF
Rosh Oxymoron
@Rosh Oxymoron: Danke für den Kommentar. Beim Schreiben habe ich explizit andfür verwendet, __eq__aber dann dachte ich: "Warum nicht Tupel verwenden?" weil ich das sowieso oft mache (ich denke es ist besser lesbar). Aus irgendeinem seltsamen Grund gingen meine Augen jedoch nicht zurück, um Fragen zu stellen __hash__.
6502
1
@ user877329: Versuchen Sie, eine Blender-Datenstruktur als Schlüssel zu verwenden? Anscheinend müssen Sie bei einigen Repos bestimmte Objekte zuerst "einfrieren", um eine Veränderbarkeit zu vermeiden (das Mutieren eines wertbasierten Objekts, das als Schlüssel in einem Python-Wörterbuch verwendet wurde, ist nicht zulässig)
6502
1
@ kawing-chiu pythonfiddle.com/eq-method-needs-ne-method <- Dies zeigt den "Fehler" in Python 2. Python 3 hat dieses Problem nicht : Der Standard __ne__()wurde "behoben" .
Bob Stein
33

Eine Alternative in Python 2.6 oder höher ist die Verwendung collections.namedtuple()- Sie können keine speziellen Methoden schreiben:

from collections import namedtuple
MyThingBase = namedtuple("MyThingBase", ["name", "location"])
class MyThing(MyThingBase):
    def __new__(cls, name, location, length):
        obj = MyThingBase.__new__(cls, name, location)
        obj.length = length
        return obj

a = MyThing("a", "here", 10)
b = MyThing("a", "here", 20)
c = MyThing("c", "there", 10)
a == b
# True
hash(a) == hash(b)
# True
a == c
# False
Sven Marnach
quelle
20

Sie überschreiben, __hash__wenn Sie eine spezielle Hash-Semantik wünschen und __cmp__oder __eq__um Ihre Klasse als Schlüssel verwendbar zu machen. Objekte, die gleich sind, müssen denselben Hashwert haben.

Python erwartet __hash__eine Ganzzahl, eine Rückgabe Banana()wird nicht empfohlen :)

Benutzerdefinierte Klassen haben __hash__standardmäßig Aufrufe id(self), wie Sie bemerkt haben.

Die Dokumentation enthält einige zusätzliche Tipps :

Klassen, die eine __hash__() Methode von einer übergeordneten Klasse erben, aber die Bedeutung von __cmp__()oder __eq__() so ändern, dass der zurückgegebene Hashwert nicht mehr angemessen ist (z. B. durch Umschalten auf ein wertbasiertes Gleichheitskonzept anstelle der identitätsbasierten Standardgleichheit), können sich explizit als kennzeichnen durch Festlegen __hash__ = None der Klassendefinition nicht verwischbar sein . Dies bedeutet, dass Instanzen der Klasse nicht nur einen geeigneten TypeError auslösen, wenn ein Programm versucht, ihren Hash-Wert abzurufen, sondern dass sie bei der Überprüfung auch korrekt als nicht verwischbar identifiziert werden isinstance(obj, collections.Hashable) (im Gegensatz zu Klassen, die ihre eigenen definieren __hash__(), um TypeError explizit auszulösen).

Skurmedel
quelle
2
Der Hash allein reicht nicht aus, zusätzlich müssen Sie entweder überschreiben __eq__oder __cmp__.
Oben Sonne
@Oben Sonne: __cmp__wird Ihnen von Python gegeben, wenn es sich um eine benutzerdefinierte Klasse handelt, aber Sie möchten sie wahrscheinlich trotzdem überschreiben, um neue Semantiken zu berücksichtigen.
Skurmedel
1
@Skurmedel: Ja, aber obwohl Sie Benutzerklassen aufrufen cmpund verwenden können, =die diese Methoden nicht überschreiben, muss eine davon implementiert werden, um die Anforderung des Fragestellers zu erfüllen, dass Instanzen mit ähnlichem Namen und Speicherort denselben Wörterbuchschlüssel haben.
Oben Sonne