Поскольку списки изменяемы, dict
ключи (и set
члены) должны быть хешируемыми, а хеширование изменяемых объектов - плохая идея, поскольку хеш-значения должны вычисляться на основе атрибутов экземпляра.
В этом ответе я приведу несколько конкретных примеров, которые, надеюсь, добавят ценности существующим ответам. Каждое понимание применимо также к элементам структуры данных set
.
Пример 1 : хеширование изменяемого объекта, где хеш-значение основано на изменяемой характеристике объекта.
>>> 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
После мутации stupid
он больше не может быть найден в dict, потому что хеш изменился. Только линейное сканирование по списку ключей dict находитstupid
.
Пример 2 : ... но почему не просто постоянное хеш-значение?
>>> 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
Это тоже не очень хорошая идея, потому что одинаковые объекты должны хешироваться одинаково, чтобы вы могли найти их в dict
илиset
.
Пример 3 : ... хорошо, а как насчет постоянных хешей во всех экземплярах ?!
>>> 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
Кажется, что все работает так, как ожидалось, но подумайте о том, что происходит: когда все экземпляры вашего класса производят одно и то же хеш-значение, у вас будет конфликт хешей всякий раз, когда имеется более двух экземпляров в качестве ключей в dict
или присутствующих в set
.
Для поиска подходящего экземпляра с помощью my_dict[key]
or key in my_dict
(или item in my_set
) необходимо выполнить столько проверок равенства, сколько экземпляров stupidlist3
в ключах dict (в худшем случае). На этом этапе цель словаря - поиск O (1) - полностью нарушена. Это продемонстрировано в следующих временных интервалах (сделано с IPython).
Некоторые сроки для примера 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)
Как видите, тест на членство в нашем тесте stupidlists_set
даже медленнее, чем линейное сканирование в целом lists_list
, в то время как у вас есть ожидаемое сверхбыстрое время поиска (фактор 500) в наборе без множества хеш-коллизий.
TL; DR: вы можете использовать их в tuple(yourlist)
качестве dict
ключей, потому что кортежи неизменяемы и хешируются.