Como as listas são mutáveis, as dictchaves (e os setmembros) 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 setestrutura 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 dictou 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 dictou 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 dictchaves, porque as tuplas são imutáveis e hashable.