Debido a que las listas son mutables, las dict
claves (y los set
miembros) deben ser hash, y el hash de objetos mutables es una mala idea porque los valores hash deben calcularse sobre la base de atributos de instancia.
En esta respuesta, daré algunos ejemplos concretos, con suerte agregando valor a las respuestas existentes. Cada conocimiento se aplica también a los elementos de la set
estructura de datos.
Ejemplo 1 : hash de un objeto mutable donde el valor hash se basa en una característica mutable del objeto.
>>> 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
Después de mutar stupid
, ya no se puede encontrar en el dict porque el hash cambió. Solo un escaneo lineal sobre la lista de claves del dict encuentra stupid
.
Ejemplo 2 : ... pero ¿por qué no solo un valor hash constante?
>>> 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
Tampoco es una buena idea porque los objetos iguales deben tener un hash idéntico de modo que pueda encontrarlos en un dict
o set
.
Ejemplo 3 : ... ok, ¿qué pasa con los hash constantes en todas las instancias?
>>> 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
Las cosas parecen funcionar como se esperaba, pero piense en lo que está sucediendo: cuando todas las instancias de su clase producen el mismo valor hash, tendrá una colisión hash siempre que haya más de dos instancias como claves en a dict
o presentes en a set
.
Encontrar la instancia correcta con my_dict[key]
o key in my_dict
(o item in my_set
) necesita realizar tantas verificaciones de igualdad como instancias de stupidlist3
en las claves del dict (en el peor de los casos). En este punto, el propósito del diccionario - búsqueda O (1) - está completamente derrotado. Esto se demuestra en los siguientes tiempos (realizados con IPython).
Algunos tiempos para el ejemplo 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)
Como puede ver, la prueba de membresía en nuestro stupidlists_set
es incluso más lenta que un escaneo lineal en su totalidad lists_list
, mientras que tiene el tiempo de búsqueda súper rápido esperado (factor 500) en un conjunto sin muchas colisiones de hash.
TL; DR: se puede usar tuple(yourlist)
como dict
claves, porque las tuplas son inmutables y se pueden usar con hash.