Objet de type personnalisé comme clé de dictionnaire

190

Que dois-je faire pour utiliser mes objets d'un type personnalisé comme clés dans un dictionnaire Python (où je ne veux pas que l '"identifiant d'objet" agisse comme clé), par exemple

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

Je voudrais utiliser MyThing comme des clés considérées comme identiques si le nom et l'emplacement sont les mêmes. Depuis C # / Java, j'ai l'habitude de devoir remplacer et de fournir une méthode d'égalité et de hashcode, et je promets de ne pas muter quoi que ce soit dont le hashcode dépend.

Que dois-je faire en Python pour accomplir cela? Dois-je même?

(Dans un cas simple, comme ici, il serait peut-être préférable de simplement placer un tuple (nom, emplacement) comme clé - mais considérez que je voudrais que la clé soit un objet)

Anonyme
la source
Quel est le problème avec l'utilisation du hachage?
Rafe Kettler le
5
Probablement parce qu'il en veut deux MyThing, s'ils ont le même nameet location, pour indexer le dictionnaire pour renvoyer la même valeur, même s'ils ont été créés séparément comme deux "objets" différents.
Santa le
1
"peut-être serait-il préférable de simplement placer un tuple (nom, emplacement) comme clé - mais considérez que je voudrais que la clé soit un objet)" Vous voulez dire: un objet NON COMPOSITE?
eyquem

Réponses:

231

Vous devez ajouter 2 méthodes , notez __hash__et __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)

La documentation Python dict définit ces exigences sur les objets clés, c'est-à-dire qu'ils doivent être hachables .

6502
la source
18
hash(self.name)semble plus agréable que self.name.__hash__(), et si vous le faites et vous pouvez le faire hash((x, y))pour éviter de vous faire du XOR.
Rosh Oxymoron le
5
Comme note supplémentaire, je viens de découvrir qu'appeler x.__hash__()comme ça est également faux , car cela peut produire des résultats incorrects : pastebin.com/C9fSH7eF
Rosh Oxymoron
@Rosh Oxymoron: merci pour le commentaire. Lors de l'écriture, j'utilisais expliciteand pour __eq__mais j'ai pensé "pourquoi ne pas utiliser de tuples?" parce que je fais souvent ça de toute façon (je pense que c'est plus lisible). Pour une raison étrange, mes yeux ne sont pas retournés à la question __hash__.
6502
1
@ user877329: essayez-vous d'utiliser une structure de données de Blender comme clés? Apparemment, à partir de certains dépôts, certains objets vous obligent à les «geler» d'abord pour éviter la mutabilité (la mutation d'un objet basé sur des valeurs qui a été utilisé comme clé dans un dictionnaire python n'est pas autorisée)
6502
1
@ kawing-chiu pythonfiddle.com/eq-method-needs-ne-method <- cela montre le "bogue" dans Python 2. Python 3 n'a pas ce problème : la valeur par défaut __ne__()a été "corrigée" .
Bob Stein
35

Une alternative dans Python 2.6 ou supérieur est d'utiliser collections.namedtuple()- cela vous évite d'écrire des méthodes spéciales:

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
la source
20

Vous remplacez __hash__si vous voulez une sémantique de hachage spéciale, et / __cmp__ou __eq__afin de rendre votre classe utilisable comme clé. Les objets qui comparent la même valeur doivent avoir la même valeur de hachage.

Python s'attend __hash__à renvoyer un entier, le retour Banana()n'est pas recommandé :)

Les classes définies par l'utilisateur ont __hash__par défaut qui appelle id(self), comme vous l'avez noté.

Il y a quelques conseils supplémentaires dans la documentation .:

Les classes qui héritent d'une __hash__() méthode d'une classe parente mais changent la signification de __cmp__()ou de __eq__() telle sorte que la valeur de hachage retournée n'est plus appropriée (par exemple en passant à un concept d'égalité basé sur une valeur au lieu de l'égalité basée sur l'identité par défaut) peuvent se signaler explicitement comme étant indéchiffrable en définissant __hash__ = None dans la définition de classe. Cela signifie que non seulement les instances de la classe lèveront une TypeError appropriée lorsqu'un programme tente de récupérer leur valeur de hachage, mais qu'elles seront également correctement identifiées comme non hachables lors de la vérification isinstance(obj, collections.Hashable) (contrairement aux classes qui définissent les leurs __hash__()pour déclencher explicitement TypeError).

Skurmedel
la source
2
Le hachage seul ne suffit pas, vous devez en outre remplacer __eq__ou __cmp__.
Oben Sonne le
@Oben Sonne: vous __cmp__est donné par Python s'il s'agit d'une classe définie par l'utilisateur, mais vous voudrez probablement les remplacer quand même pour tenir compte de la nouvelle sémantique.
Skurmedel le
1
@Skurmedel: Oui, mais bien que vous puissiez appeler cmpet utiliser =des classes d'utilisateurs qui ne remplacent pas ces méthodes, l'une d'elles doit être implémentée pour répondre à l'exigence du questionneur voulant que les instances avec un nom et un emplacement similaires aient la même clé de dictionnaire.
Oben Sonne le