Qual è un modo corretto e buono per implementare __hash __ ()?


150

Qual è un modo corretto e buono per implementare __hash__()?

Sto parlando della funzione che restituisce un hashcode che viene quindi utilizzato per inserire oggetti in dizionari hashtabili.

Poiché __hash__()restituisce un numero intero e viene utilizzato per "binning" di oggetti in hashtable, suppongo che i valori dell'intero restituito debbano essere distribuiti uniformemente per i dati comuni (per ridurre al minimo le collisioni). Qual è una buona pratica per ottenere tali valori? Le collisioni sono un problema? Nel mio caso ho una piccola classe che funge da classe contenitore con alcuni ints, alcuni float e una stringa.

Risposte:


185

Un modo semplice e corretto di implementare __hash__()è utilizzare una tupla chiave. Non sarà veloce come un hash specializzato, ma se ne hai bisogno, probabilmente dovresti implementare il tipo in C.

Ecco un esempio dell'uso di una chiave per l'hash e l'uguaglianza:

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

Inoltre, la documentazione di__hash__ ha più informazioni, che possono essere utili in alcune circostanze particolari.


1
A parte il piccolo sovraccarico derivante dal factoring della __keyfunzione, questo è veloce quanto qualsiasi hash può essere. Certo, se si sa che gli attributi sono numeri interi e non ce ne sono troppi, suppongo che potresti potenzialmente correre leggermente più veloce con un po 'di hash roll a casa, ma probabilmente non sarebbe altrettanto ben distribuito. hash((self.attr_a, self.attr_b, self.attr_c))sarà sorprendentemente veloce (e corretto ), poiché la creazione di piccole tuples è appositamente ottimizzata e spinge il lavoro di ottenere e combinare gli hash con i builtin C, che è in genere più veloce del codice di livello Python.
ShadowRanger

Supponiamo che un oggetto di classe A sia usato come chiave per un dizionario e se un attributo di classe A cambia, anche il suo valore di hash cambierà. Non creerebbe un problema?
Mr Matrix,

1
Come menzionato nella risposta di @ loved.by.Jesus di seguito, il metodo hash non deve essere definito / ignorato per un oggetto mutabile (definito per impostazione predefinita e utilizza id per l'uguaglianza e il confronto).
Mr Matrix,

@Miguel, ho riscontrato il problema esatto , ciò che accade è che il dizionario ritorna Noneuna volta cambiata la chiave. Il modo in cui l'ho risolto è stato memorizzando l'id dell'oggetto come chiave anziché solo l'oggetto.
Jaswant P

@JaswantP Python per impostazione predefinita utilizza l'id dell'oggetto come chiave per qualsiasi oggetto hash.
Mr Matrix,

22

John Millikin ha proposto una soluzione simile a questa:

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))

Il problema con questa soluzione è che hash(A(a, b, c)) == hash((a, b, c)). In altre parole, l'hash si scontra con quello della tupla dei suoi membri chiave. Forse questo non importa molto spesso in pratica?

Aggiornamento: i documenti di Python ora raccomandano di usare una tupla come nell'esempio sopra. Si noti che la documentazione afferma

L'unica proprietà richiesta è che gli oggetti che si equivalgono hanno lo stesso valore hash

Si noti che non è vero il contrario. Gli oggetti che non si equivalgono possono avere lo stesso valore hash. Una tale collisione hash non farà sì che un oggetto ne sostituisca un altro quando viene usato come chiave dict o imposta elemento purché gli oggetti non siano uguali .

Soluzione obsoleta / non valida

La documentazione di Python su__hash__ suggerisce di combinare gli hash dei sottocomponenti usando qualcosa come XOR , che ci dà questo:

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)))

Aggiornamento: come sottolinea Blckknght, cambiare l'ordine di a, b e c potrebbe causare problemi. Ho aggiunto un ulteriore ^ hash((self._a, self._b, self._c))per catturare l'ordine dei valori sottoposti a hash. Questo finale ^ hash(...)può essere rimosso se i valori combinati non possono essere riorganizzati (ad esempio, se hanno tipi diversi e quindi il valore di _anon verrà mai assegnato a _bo _c, ecc.).


5
Di solito non vuoi fare un XOR dritto degli attributi insieme, poiché ciò ti darà delle collisioni se cambi l'ordine dei valori. Cioè, hash(A(1, 2, 3))sarà uguale a hash(A(3, 1, 2))(e avranno entrambi l'hash uguale a qualsiasi altra Aistanza con una permutazione di 1, 2e 3come i suoi valori). Se vuoi evitare che la tua istanza abbia lo stesso hash di una tupla dei loro argomenti, crea semplicemente un valore sentinella (come variabile di classe o come globale), quindi includilo nella tupla da hash: return hash ((_ sentinel , self._a, self._b, self._c))
Blckknght,

1
L'uso di isinstancepotrebbe essere problematico, poiché un oggetto di una sottoclasse di type(self)può ora essere uguale a un oggetto di type(self). Quindi potresti scoprire che l'aggiunta di a Care a Forda set()può comportare l'inserimento di un solo oggetto, a seconda dell'ordine di inserimento. Inoltre, potresti imbatterti in una situazione in cui a == bè vero ma b == aè falso.
MaratC

1
Se stai Bisinstance(othr, B)
eseguendo la

7
Un pensiero: tupla chiave potrebbe includere il tipo di classe, che impedirebbe altre classi con lo stesso insieme di attributi chiave vengano visualizzati dai pari: hash((type(self), self._a, self._b, self._c)).
Ben Mosher,

2
Oltre al punto sull'uso Binvece di type(self), è anche spesso considerata una buona pratica tornare NotImplementedquando si incontra un tipo inaspettato __eq__invece di False. Ciò consente ad altri tipi definiti dall'utente di implementare un oggetto __eq__che conosce Be può confrontare uguale ad esso, se lo desidera.
Mark Amery,

16

Paul Larson di Microsoft Research ha studiato un'ampia varietà di funzioni hash. Lui mi ha detto che

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

ha funzionato sorprendentemente bene per un'ampia varietà di stringhe. Ho scoperto che tecniche polinomiali simili funzionano bene per calcolare un hash di sottocampi disparati.


8
Apparentemente Java lo fa allo stesso modo ma usando 31 anziché 101
user229898

3
Qual è la logica dietro l'utilizzo di questi numeri? C'è un motivo per scegliere 101 o 31?
bigblind

1
Ecco una spiegazione per i moltiplicatori primi: stackoverflow.com/questions/3613102/… . 101 sembra funzionare particolarmente bene, basato sugli esperimenti di Paul Larson.
George V. Reilly,

4
Python utilizza (hash * 1000003) XOR ord(c)per le stringhe con moltiplicazione avvolgente a 32 bit. [Citazione ]
tylerl

4
Anche se questo è vero, non è di alcuna utilità pratica in questo contesto perché i tipi di stringa Python integrati forniscono già un __hash__metodo; non abbiamo bisogno di fare il nostro. La domanda è come implementare __hash__una tipica classe definita dall'utente (con un gruppo di proprietà che punta a tipi predefiniti o forse ad altre classi definite dall'utente), che questa risposta non risolve affatto.
Mark Amery,

3

Posso provare a rispondere alla seconda parte della tua domanda.

Le collisioni probabilmente non deriveranno dal codice hash stesso, ma dalla mappatura del codice hash su un indice in una raccolta. Quindi, ad esempio, la tua funzione hash potrebbe restituire valori casuali da 1 a 10000, ma se la tua tabella hash ha solo 32 voci otterrai collisioni all'inserimento.

Inoltre, riterrei che le collisioni vengano risolte internamente dalla raccolta e ci sono molti metodi per risolvere le collisioni. Il più semplice (e il peggio) è, data una voce da inserire all'indice i, aggiungi 1 a i fino a trovare un punto vuoto e inserirlo lì. Il recupero funziona quindi allo stesso modo. Ciò si traduce in recuperi inefficienti per alcune voci, in quanto potresti trovare una voce che richiede di attraversare l'intera raccolta per trovare!

Altri metodi di risoluzione delle collisioni riducono i tempi di recupero spostando le voci nella tabella hash quando un elemento viene inserito per distribuire le cose. Ciò aumenta il tempo di inserimento ma presuppone che tu legga più di quello che inserisci. Esistono anche metodi che tentano di ramificare diverse voci in collisione in modo che le voci si raggruppino in un punto specifico.

Inoltre, se devi ridimensionare la raccolta, dovrai ripassare tutto o utilizzare un metodo di hashing dinamico.

In breve, a seconda di cosa stai usando il codice hash per te potrebbe essere necessario implementare il tuo metodo di risoluzione delle collisioni. Se non li memorizzi in una raccolta, probabilmente puoi cavartela con una funzione hash che genera codici hash in un intervallo molto ampio. In tal caso, puoi assicurarti che il tuo contenitore sia più grande di quanto debba essere (più grande è, ovviamente, meglio) a seconda delle preoccupazioni relative alla memoria.

Ecco alcuni link se sei interessato di più:

hashing coalizzato su wikipedia

Wikipedia ha anche un riepilogo di vari metodi di risoluzione delle collisioni:

Inoltre, " Organizzazione ed elaborazione dei file " di Tharp copre ampiamente numerosi metodi di risoluzione delle collisioni. IMO è un ottimo riferimento per gli algoritmi di hashing.


1

Un'ottima spiegazione su quando e come implementare la __hash__funzione sul sito Web di Programiz :

Solo uno screenshot per fornire una panoramica: (Estratto 13-12-2019)

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

Per quanto riguarda un'implementazione personale del metodo, il sito sopra menzionato fornisce un esempio che corrisponde alla risposta di millerdev .

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))

0

Dipende dalla dimensione del valore hash restituito. È semplice logica che se hai bisogno di restituire un int a 32 bit basato sull'hash di quattro in 32 bit, otterrai collisioni.

Preferirei operazioni bit. Ad esempio, il seguente pseudo codice C:

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

Un tale sistema potrebbe funzionare anche per i float, se li prendessi semplicemente come valore in bit anziché rappresentare effettivamente un valore in virgola mobile, forse meglio.

Per quanto riguarda le stringhe, ho poca / nessuna idea.


So che ci saranno collisioni. Ma non ho idea di come vengano gestiti. Inoltre, i miei valori degli attributi in combinazione sono distribuiti in modo molto limitato, quindi stavo cercando una soluzione intelligente. E in qualche modo mi aspettavo che ci fosse una buona pratica da qualche parte.
user229898
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.