2012-04-20 21 views
23

Il docs afferma che una classe è lavabile finché definisce il metodo __hash__ e il metodo __eq__. Tuttavia:Che cosa rende inaffidabile una classe definita dall'utente?

class X(list): 
    # read-only interface of `tuple` and `list` should be the same, so reuse tuple.__hash__ 
    __hash__ = tuple.__hash__ 

x1 = X() 
s = {x1} # TypeError: unhashable type: 'X' 

Cosa rende X inattivo?

Si noti che è necessario disporre di elenchi identici (in termini di uguaglianza regolare) da sottoporre a hash allo stesso valore; altrimenti, io violate this requirement su funzioni hash:

L'unica proprietà richiesta è che gli oggetti che risultano uguali hanno lo stesso valore di hash

i documenti non avvertono che un oggetto hashabile non deve essere modificato durante la sua durata, e ovviamente non modifico le istanze di X dopo la creazione. Ovviamente, l'interprete non lo controllerà comunque.

+2

Sì, le interfacce di sola lettura sono gli stessi, ma perché si aspetta tuple .__ hash__ di usare solo le interfacce esterne di essa la propria classe? Soprattutto se scritto in C. L'uso delle interfacce esterne sarebbe molto più lento. Non puoi ragionevolmente aspettarti che un metodo dalla classe A funzioni per la classe B a meno che la classe B non sia sottoclassata dalla classe A. Hai provato a chiamare anche x1 .__ hash __() per vedere se ha funzionato? –

+0

@LennartRegebro Sì, sono d'accordo ... Vedere il mio ultimo commento a http: //stackoverflow.com/a/10254636/336527 ... Ho appena avuto un blocco del cervello. – max

risposta

15

Impostare semplicemente il metodo __hash__ su quello della classe tuple non è sufficiente. Non hai effettivamente detto come hash in modo diverso. le tuple sono lavabili perché sono immutabili. Se si voleva davvero per farvi lavoro specifico esempio, potrebbe essere simile a questo:

class X2(list): 
    def __hash__(self): 
     return hash(tuple(self)) 

In questo caso si sono effettivamente definendo come hash vostro elenco personalizzato sottoclasse. Devi solo definire esattamente come può generare un hash. È possibile hash su quello che vuoi, anziché utilizzare il metodo di hashing della tupla:

def __hash__(self): 
    return hash("foobar"*len(self)) 
+0

Ma non è 'tuple .__ hash__' una funzione che accetta una tupla e restituisce un numero? Come fa questa funzione a "notare" che il mio oggetto è in realtà un 'elenco' piuttosto che una' tupla' - l'API di lettura per i due tipi è identica. – max

+0

@max: 'tuple .__ hash__' è un metodo associato della classe tuple. Non stai cambiando qualunque cosa stia facendo l'implementazione all'interno di quel metodo all'hash. Definisci il tuo. – jdi

+0

'hash ((1,2,3))' è uguale a '(1,2,3) .__ hash__'; è uguale a 'tuple .__ hash __ ((1,2,3))', giusto? Quindi 'tuple .__ hash__' si basa sull'API non pubblica della classe' tuple', e quindi si rompe con un messaggio di errore confuso quando viene passata un'istanza di una classe diversa che corrisponde all'API pubblica di 'tuple'? Suppongo che lo spieghi .. ma un po 'inaspettato. – max

3

Se non si modificano le istanze di X dopo la creazione, perché non è vero sottoclasse tupla?

Ma sottolineo che questo in realtà non genera un errore, almeno in Python 2.6.

Ho esitato a dire "funziona" perché questo non fa quello che pensi che faccia.

>>> a = X() 
>>> b = X((5,)) 
>>> hash(a) 
4299954584 
>>> hash(b) 
4299954672 
>>> id(a) 
4299954584 
>>> id(b) 
4299954672 

Sta solo usando l'ID oggetto come hash. Quando si chiama in realtà __hash__ si ottiene ancora un errore; allo stesso modo per __eq__.

>>> a.__hash__() 
Traceback (most recent call last): 
    File "<stdin>", line 1, in <module> 
TypeError: descriptor '__hash__' for 'tuple' objects doesn't apply to 'X' object 
>>> X().__eq__(X()) 
Traceback (most recent call last): 
    File "<stdin>", line 1, in <module> 
TypeError: descriptor '__eq__' for 'tuple' objects doesn't apply to 'X' object 

ho capito che i meccanismi interni di pitone, per qualche ragione, stanno rilevando che X ha un __hash__ e un metodo di __eq__, ma non li stanno chiamando.

La morale di tutto questo è: basta scrivere una vera funzione hash. Poiché si tratta di un oggetto sequenza, convertirlo in tupla e hashing è l'approccio più ovvio.

def __hash__(self): 
    return hash(tuple(self)) 
+0

Mi dispiace molto, questa domanda è presa fuori dal contesto di un altro. Ero solo confuso su questo particolare comportamento. Il motivo per cui elenco di sottoclassi è un po 'complicato (vedi la discussione nei commenti a [questa domanda] (http://stackoverflow.com/questions/10253783/making-a-list-subclass-hashable)). – max

+0

Il codice non funziona per me in ActiveState Python 3.2. Forse il comportamento è cambiato di recente? – max

+0

Sto usando Python 2.6. In ogni caso, non vuoi questo comportamento, perché usare 'id's come chiavi non è davvero una buona idea. Meglio solo convertire in tuple e hash. E in realtà - mi dispiace; questo era solo un approccio piuttosto perplesso al problema per me. – senderle

5

Che cosa si potrebbe e dovrebbe fare, in base alla tua altra domanda, è: non sottoclasse nulla, solo incapsulare una tupla. È perfettamente bene farlo nell'iniz.

class X(object): 
    def __init__(self, *args): 
     self.tpl = args 
    def __hash__(self): 
     return hash(self.tpl) 
    def __eq__(self, other): 
     return self.tpl == other 
    def __repr__(self): 
     return repr(self.tpl) 

x1 = X() 
s = {x1} 

che produce:

>>> s 
set([()]) 
>>> x1 
() 
+0

Hai ragione, per molti casi d'uso questa è la soluzione più semplice e pulita; +1 – senderle

4

Dalla documentazione python3:

Se una classe non definisce un metodo __eq __() non deve definire un'operazione __hash __() sia ; se definisce __eq __() ma non __hash __(), le sue istanze non saranno utilizzabili come elementi nelle raccolte hasable. Se una classe definisce gli oggetti mutabili e implementa un metodo __eq __() , non dovrebbe implementare __hash __(), poiché l'implementazione delle collezioni hastable richiede che il valore hash della chiave sia immutabile (se il valore dell'hash dell'oggetto cambia, sarà in il secchio hash sbagliato).

Rif: object.__hash__(self)

codice di esempio:

class Hashable: 
    pass 

class Unhashable: 
    def __eq__(self, other): 
     return (self == other) 

class HashableAgain: 
    def __eq__(self, other): 
     return (self == other) 

    def __hash__(self): 
     return id(self) 

def main(): 
    # OK 
    print(hash(Hashable())) 
    # Throws: TypeError("unhashable type: 'X'",) 
    print(hash(Unhashable())) 
    # OK 
    print(hash(HashableAgain())) 
+0

Il '__hash__' deve essere unico? Si supponga di volere confrontare le istanze di 'HashableAgain' in base ai criteri definiti in' __eq__', si può semplicemente restituire un numero intero costante in '__hash__'? (Non capisco davvero come hash) viene utilizzato nel decidere l'appartenenza di un oggetto in un set. –

+0

@MinhTran: In generale, l'hash non è univoco, ma _relativamente_ univoco. È usato per memorizzare i valori in una mappa. Se si utilizza un valore costante per l'hash, tutti i valori verranno visualizzati nello stesso bucket, quindi le prestazioni saranno orribili ... ma dovrebbe comunque funzionare! – kevinarpe