Oggetto di tipo personalizzato come chiave del dizionario


185

Cosa devo fare per usare i miei oggetti di tipo personalizzato come chiavi in ​​un dizionario Python (dove non voglio che l '"ID oggetto" funga da chiave), ad es.

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

Vorrei usare MyThing come chiavi che sono considerate uguali se il nome e la posizione sono uguali. Da C # / Java sono abituato a dover sovrascrivere e fornire un metodo uguale e hashcode e prometto di non mutare nulla da cui dipende l'hashcode.

Cosa devo fare in Python per ottenere questo risultato? Dovrei anche?

(In un caso semplice, come qui, forse sarebbe meglio posizionare una tupla (nome, posizione) come chiave - ma considera che vorrei che la chiave fosse un oggetto)


Cosa c'è di sbagliato nell'uso dell'hash?
Rafe Kettler,

5
Probabilmente perché ne vuole due MyThing, se hanno lo stesso namee location, per indicizzare il dizionario per restituire lo stesso valore, anche se sono stati creati separatamente come due "oggetti" diversi.
Santa

1
"forse sarebbe meglio posizionare una tupla (nome, posizione) come chiave - ma considera che vorrei che la chiave fosse un oggetto)" Intendi dire: un oggetto NON COMPOSITO?
eyquem,

Risposte:


221

Devi aggiungere 2 metodi , nota __hash__e __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 documentazione di Python dict definisce questi requisiti sugli oggetti chiave, cioè devono essere hash .


17
hash(self.name)sembra più bello di self.name.__hash__(), e se lo fai e puoi fare hash((x, y))per evitare XORing te stesso.
Rosh Oxymoron,

5
Come nota aggiuntiva, ho appena scoperto che anche chiamare in x.__hash__()quel modo è sbagliato , perché può produrre risultati errati : pastebin.com/C9fSH7eF
Rosh Oxymoron

@Rosh Oxymoron: grazie per il commento. Quando scrivevo stavo usando esplicito andper __eq__ma poi ho pensato "perché non usare le tuple?" perché lo faccio spesso (penso che sia più leggibile). Per qualche strana ragione, tuttavia, i miei occhi non sono tornati in discussione __hash__.
6502

1
@ user877329: stai cercando di usare una struttura di dati di Blender come chiavi? Apparentemente da alcuni repository alcuni oggetti richiedono prima di "congelarli" per evitare la mutabilità (non è permesso mutare un oggetto basato sul valore che è stato usato come chiave in un dizionario python)
6502

1
@ kawing-chiu pythonfiddle.com/eq-method-needs-ne-method <- questo mostra il "bug" in Python 2. Python 3 non ha questo problema : il default __ne__()è stato "riparato" .
Bob Stein,

34

Un'alternativa in Python 2.6 o versioni successive è l'uso collections.namedtuple(): consente di risparmiare la scrittura di metodi speciali:

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

20

Sostituisci __hash__se vuoi una semantica hash speciale e __cmp__o __eq__per rendere la tua classe utilizzabile come chiave. Gli oggetti che hanno lo stesso confronto devono avere lo stesso valore hash.

Python prevede __hash__di restituire un numero intero, la restituzione Banana()non è consigliata :)

Le classi definite dall'utente hanno __hash__per impostazione predefinita chiamate che id(self), come hai notato.

Ci sono alcuni suggerimenti extra dalla documentazione .:

Le classi che ereditano un __hash__() metodo da una classe genitore ma cambiano il significato __cmp__()o in modo __eq__() tale che il valore hash restituito non sia più appropriato (ad esempio passando a un concetto di uguaglianza basato sul valore anziché all'uguaglianza basata sull'identità predefinita) possono esplicitamente contrassegnarsi come essendo sfasabile impostando __hash__ = None nella definizione della classe. Ciò significa che non solo le istanze della classe genereranno un TypeError appropriato quando un programma tenta di recuperare il loro valore hash, ma saranno anche correttamente identificate come non lavabili durante il controllo isinstance(obj, collections.Hashable) (a differenza delle classi che definiscono le proprie __hash__()per aumentare esplicitamente TypeError).


2
L'hash da solo non è sufficiente, inoltre è necessario eseguire l'override __eq__o __cmp__.
Oben Sonne,

@Oben Sonne: ti __cmp__viene fornito da Python se si tratta di una classe definita dall'utente, ma probabilmente vorrai comunque sostituirli per adattarli alla nuova semantica.
Skurmedel,

1
@Skurmedel: Sì, ma sebbene sia possibile chiamare cmpe utilizzare =su classi utente che non sovrascrivono questi metodi, è necessario implementarne uno per soddisfare il requisito dell'interrogante secondo cui le istanze con nome e posizione simili hanno la stessa chiave del dizionario.
Oben Sonne,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.