Étant donné que les listes sont mutables, les dict
clés (et les set
membres) doivent être hachables et le hachage d'objets mutables est une mauvaise idée car les valeurs de hachage doivent être calculées sur la base des attributs d'instance.
Dans cette réponse, je donnerai quelques exemples concrets, j'espère ajouter de la valeur en plus des réponses existantes. Chaque aperçu s'applique également aux éléments de la set
structure de données.
Exemple 1 : hachage d'un objet mutable où la valeur de hachage est basée sur une caractéristique mutable de l'objet.
>>> 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
Après la mutation stupid
, il ne peut plus être trouvé dans le dict car le hachage a changé. Seul un balayage linéaire sur la liste des clés du dict est trouvé stupid
.
Exemple 2 : ... mais pourquoi pas simplement une valeur de hachage 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
Ce n'est pas non plus une bonne idée car les objets égaux doivent hacher de manière identique pour que vous puissiez les trouver dans un dict
ou set
.
Exemple 3 : ... ok, qu'en est-il des hachages constants sur toutes les instances?!
>>> 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
Les choses semblent fonctionner comme prévu, mais pensez à ce qui se passe: lorsque toutes les instances de votre classe produisent la même valeur de hachage, vous aurez une collision de hachage chaque fois qu'il y a plus de deux instances en tant que clés dans a dict
ou présentes dans a set
.
Trouver la bonne instance avec my_dict[key]
ou key in my_dict
(ou item in my_set
) nécessite d'effectuer autant de contrôles d'égalité qu'il y a d'instances destupidlist3
dans les clés du dict (dans le pire des cas). À ce stade, le but du dictionnaire - la recherche O (1) - est complètement vaincu. Ceci est démontré dans les timings suivants (fait avec IPython).
Quelques horaires pour l'exemple 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)
Comme vous pouvez le voir, le test d'appartenance à notre stupidlists_set
est encore plus lent qu'un balayage linéaire sur l'ensemblelists_list
, alors que vous avez le temps de recherche super rapide attendu (facteur 500) dans un ensemble sans beaucoup de collisions de hachage.
TL; DR: vous pouvez utiliser tuple(yourlist)
comme dict
clés, car les tuples sont immuables et hachables.