Poiché gli elenchi sono modificabili, le dict
chiavi (e i set
membri) devono essere modificabili e l'hashing di oggetti modificabili è una cattiva idea perché i valori hash dovrebbero essere calcolati sulla base degli attributi dell'istanza.
In questa risposta, fornirò alcuni esempi concreti, auspicabilmente aggiungendo valore alle risposte esistenti. Ogni intuizione si applica anche agli elementi della struttura set
dati.
Esempio 1 : hashing di un oggetto modificabile in cui il valore hash è basato su una caratteristica mutabile dell'oggetto.
>>> class stupidlist(list):
... def __hash__(self):
... return len(self)
...
>>> stupid = stupidlist([1, 2, 3])
>>> d = {stupid: 0}
>>> stupid.append(4)
>>> stupid
[1, 2, 3, 4]
>>> d
{[1, 2, 3, 4]: 0}
>>> stupid in d
False
>>> stupid in d.keys()
False
>>> stupid in list(d.keys())
True
Dopo la modifica stupid
, non può più essere trovato nel dict perché l'hash è cambiato. Trova solo una scansione lineare sull'elenco delle chiavi del dict stupid
.
Esempio 2 : ... ma perché non solo un valore hash costante?
>>> class stupidlist2(list):
... def __hash__(self):
... return id(self)
...
>>> stupidA = stupidlist2([1, 2, 3])
>>> stupidB = stupidlist2([1, 2, 3])
>>>
>>> stupidA == stupidB
True
>>> stupidA in {stupidB: 0}
False
Anche questa non è una buona idea perché gli oggetti uguali dovrebbero essere hash in modo identico in modo da poterli trovare in un dict
o set
.
Esempio 3 : ... ok, che dire degli hash costanti in tutte le istanze ?!
>>> class stupidlist3(list):
... def __hash__(self):
... return 1
...
>>> stupidC = stupidlist3([1, 2, 3])
>>> stupidD = stupidlist3([1, 2, 3])
>>> stupidE = stupidlist3([1, 2, 3, 4])
>>>
>>> stupidC in {stupidD: 0}
True
>>> stupidC in {stupidE: 0}
False
>>> d = {stupidC: 0}
>>> stupidC.append(5)
>>> stupidC in d
True
Le cose sembrano funzionare come previsto, ma pensa a cosa sta succedendo: quando tutte le istanze della tua classe producono lo stesso valore hash, avrai una collisione hash ogni volta che ci sono più di due istanze come chiavi in a dict
o presenti in a set
.
Trovare l'istanza giusta con my_dict[key]
o key in my_dict
(o item in my_set
) deve eseguire tanti controlli di uguaglianza quante sono le istanze di stupidlist3
nelle chiavi del dict (nel caso peggiore). A questo punto, lo scopo del dizionario - ricerca O (1) - è completamente sconfitto. Ciò è dimostrato nelle seguenti tempistiche (eseguite con IPython).
Alcuni tempi per l'esempio 3
>>> lists_list = [[i] for i in range(1000)]
>>> stupidlists_set = {stupidlist3([i]) for i in range(1000)}
>>> tuples_set = {(i,) for i in range(1000)}
>>> l = [999]
>>> s = stupidlist3([999])
>>> t = (999,)
>>>
>>> %timeit l in lists_list
25.5 µs ± 442 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
>>> %timeit s in stupidlists_set
38.5 µs ± 61.2 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
>>> %timeit t in tuples_set
77.6 ns ± 1.5 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
Come puoi vedere, il test di appartenenza nel nostro stupidlists_set
è ancora più lento di una scansione lineare nel suo complesso lists_list
, mentre hai il tempo di ricerca super veloce previsto (fattore 500) in un set senza un sacco di collisioni hash.
TL; DR: puoi usare tuple(yourlist)
come dict
chiavi, perché le tuple sono immutabili e hash.