Como as listas são mutáveis, as dict
chaves (e os set
membros) precisam ser hash, e o hash de objetos mutáveis é uma má ideia porque os valores de hash devem ser calculados com base nos atributos da instância.
Nesta resposta, darei alguns exemplos concretos, espero agregar valor às respostas existentes. Cada percepção se aplica aos elementos da set
estrutura de dados também.
Exemplo 1 : hash de um objeto mutável onde o valor de hash é baseado em uma característica mutável do 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
Após a mutação stupid
, ele não pode mais ser encontrado no dicionário porque o hash mudou. Apenas uma varredura linear sobre a lista de achados de chaves do dict stupid
.
Exemplo 2 : ... mas por que não apenas um valor de 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
Isso também não é uma boa ideia, porque objetos iguais devem hash de forma idêntica para que você possa encontrá-los em um dict
ou set
.
Exemplo 3 : ... ok, que tal hashes constantes em todas as instâncias ?!
>>> 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
As coisas parecem funcionar conforme o esperado, mas pense no que está acontecendo: quando todas as instâncias de sua classe produzem o mesmo valor de hash, você terá uma colisão de hash sempre que houver mais de duas instâncias como chaves em a dict
ou presentes em a set
.
Encontrar a instância certa com my_dict[key]
ou key in my_dict
(ou item in my_set
) precisa realizar tantas verificações de igualdade quantas forem as instâncias destupidlist3
nas chaves do dicionário (no pior caso). Nesse ponto, o objetivo do dicionário - pesquisa O (1) - foi completamente derrotado. Isso é demonstrado nas seguintes temporizações (feitas com IPython).
Alguns tempos para o exemplo 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 você pode ver, o teste de associação em nosso stupidlists_set
é ainda mais lento do que uma varredura linear no todo lists_list
, enquanto você tem o tempo de pesquisa super rápido esperado (fator 500) em um conjunto sem muitas colisões de hash.
TL; DR: você pode usar tuple(yourlist)
como dict
chaves, porque as tuplas são imutáveis e hashable.