Почему я не могу использовать список как ключ dict в python? - PullRequest
77 голосов
/ 31 августа 2011

Я немного озадачен тем, что можно / нельзя использовать в качестве ключа для python dict.

dicked = {}
dicked[None] = 'foo'     # None ok
dicked[(1,3)] = 'baz'    # tuple ok
import sys
dicked[sys] = 'bar'      # wow, even a module is ok !
dicked[(1,[3])] = 'qux'  # oops, not allowed

Таким образом, кортеж является неизменяемым типом, но если я скрываю список внутри него, то это не может быть ключ ... не мог ли я так же легко скрыть список внутри модуля?

У меня было какое-то смутное представление о том, что ключ должен быть "хэшируемым", но я просто собираюсь признать свое собственное незнание технических деталей;Я не знаю, что на самом деле здесь происходит.Что может пойти не так, если вы попытаетесь использовать списки в качестве ключей с хешем, скажем, как место их памяти?

Ответы [ 9 ]

24 голосов
/ 31 августа 2011

Почему я не могу использовать список в качестве ключа dict в python?

>>> d = {repr([1,2,3]): 'value'}
{'[1, 2, 3]': 'value'}

(для всех, кто сталкивается с этим вопросом, ищет способ обойти это)

как объяснили здесь другие, на самом деле вы не можете.Однако вы можете вместо этого использовать его строковое представление, если вы действительно хотите использовать свой список.

21 голосов
/ 31 августа 2011

В викитоне Python есть хорошая статья на эту тему: Почему списки не могут быть словарными ключами . Как объяснено там:

Что бы пошло не так, если бы вы попытались использовать списки в качестве ключей с хешем, скажем, как место их памяти?

Это можно сделать, не нарушая никаких требований, но это приводит к неожиданному поведению. Списки обычно обрабатываются так, как будто их значение было получено из значений их содержимого, например, при проверке (не) равенства. Многие, по понятным причинам, ожидают, что вы можете использовать любой список [1, 2], чтобы получить тот же ключ, в котором вам придется хранить точно такой же объект списка. Но поиск по значениям прерывается, как только список, используемый в качестве ключа, изменяется, а для поиска по идентификатору требуется, чтобы вы держали в точности один и тот же список - что не требуется для любой другой обычной операции со списком (по крайней мере, я не могу придумать) ).

Другие объекты, такие как модули и object, в любом случае значительно больше влияют на идентичность их объектов (когда в последний раз у вас было два отдельных объекта-модуля, называемых sys?), И все равно сравниваются. Поэтому менее удивительно - или даже ожидаемо - что они, при использовании в качестве ключей dict, сравниваются по идентичности и в этом случае.

10 голосов
/ 31 августа 2011

Проблема в том, что кортежи неизменны, а списки - нет. Рассмотрим следующее

d = {}
li = [1,2,3]
d[li] = 5
li.append(4)

Что должен d[li] вернуть? Это тот же список? Как насчет d[[1,2,3]]? Он имеет те же значения, но другой список?

В конечном счете, нет удовлетворительного ответа. Например, если единственный работающий ключ - это оригинальный ключ, то, если у вас нет ссылки на этот ключ, вы никогда не сможете снова получить доступ к значению. С любым другим разрешенным ключом вы можете создать ключ без ссылки на оригинал.

Если оба моих предложения работают, то у вас есть очень разные ключи, которые возвращают одно и то же значение, что более чем удивительно. Если работает только оригинальное содержимое, ваш ключ быстро испортится, поскольку списки создаются для изменения.

7 голосов
/ 31 августа 2011

Вот ответ http://wiki.python.org/moin/DictionaryKeys

Что бы пошло не так, если бы вы попытались использовать списки в качестве ключей с хешем, скажем, как место их памяти?

Поиск разных списков с одинаковым содержимым приведет к разным результатам, даже если сравнение списков с одинаковым содержимым покажет их как эквивалентные.

Как насчет использования литерала списка в поиске по словарю?

6 голосов
/ 02 августа 2018

Только что вы можете изменить Список в кортеж, а затем использовать его в качестве ключей.

d = {tuple([1,2,3]): 'value'}
2 голосов
/ 31 августа 2011

Ваш awnser можно найти здесь:

Почему списки не могут быть словарными ключами

Новички в Python часто задаются вопросом, почему, хотя язык включает в себя оба кортеж и тип списка, кортежи можно использовать как ключи словаря, в то время как списков нет. Это было обдуманное дизайнерское решение, и лучше всего объяснил сначала понимание того, как работают словари Python.

Источник и дополнительная информация: http://wiki.python.org/moin/DictionaryKeys

1 голос
/ 26 октября 2018

Поскольку списки являются изменяемыми, ключи dictset члены) должны быть хешируемыми, а хеширование изменяемых объектов - плохая идея, поскольку хеш-значения должны вычисляться на основе атрибутов экземпляра. .

В этом ответе я приведу несколько конкретных примеров, надеюсь, добавляя ценность поверх существующих ответов. Каждое понимание относится и к элементам структуры данных 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 его больше нельзя найти в диктовке, поскольку хэш изменился. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Стр. 101

Пример 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] или key in my_dict (или item in my_set) необходимо выполнить столько проверок на равенство, сколько имеется экземпляров stupidlist3 в ключах диктовки (в худшем случае). На этом этапе цель словаря - поиск 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 ключи, потому что кортежи являются неизменяемыми и хэшируемыми.

1 голос
/ 31 августа 2011

Простой ответ на ваш вопрос заключается в том, что в списке классов не реализован метод hash , который требуется для любого объекта, который требуется использовать в качестве ключа в словаре.Однако причина, по которой хэш реализован не так, как, скажем, класс кортежа (основанный на содержимом контейнера), заключается в том, что список изменчив, поэтому редактирование списка потребует пересчета хеша.это может означать, что список в настоящее время находится в неправильном сегменте в подчиненной хэш-таблице.Обратите внимание, что, поскольку вы не можете изменить кортеж (неизменяемый), он не сталкивается с этой проблемой.

В качестве примечания, фактическая реализация поиска диктобъектов основана на алгоритме D из Knuth Vol.3, гл.6.4.Если вам доступна эта книга, ее стоит почитать, кроме того, если вы действительно, действительно заинтересованы, вы можете взглянуть на комментарии разработчиков по фактической реализации dictobject здесь. В нем подробно рассказывается, как именно это работает.Существует также Python лекция о реализации словарей, которые могут вас заинтересовать. Они проходят определение ключа и что такое хеш в первые несколько минут.

0 голосов
/ 31 августа 2011

В соответствии с документацией Python 2.7.2:

Объект является хэшируемым, если у него есть значение хеша, которое никогда не изменяется в течение времени его существования (ему требуется хеш ()метод), и его можно сравнить с другими объектами (для этого требуется метод eq () или cmp ()).Хэшируемые объекты, которые сравниваются равными, должны иметь одно и то же значение хеш-функции.

Хэшируемость делает объект пригодным для использования в качестве ключа словаря и члена набора, поскольку эти структуры данных используют значение хеш-функции для внутреннего использования.Неизменяемые встроенные объекты Python являются хэшируемыми, в то время как нет изменяемых контейнеров (таких как списки или словари).Объекты, которые являются экземплярами пользовательских классов, по умолчанию могут быть хэшируемыми;все они сравниваются неравно, и их хэш-значением является их id ().

Кортеж является неизменным в том смысле, что вы не можете добавлять, удалять или заменять его элементы, но сами элементы могут быть изменяемыми.Хэш-значение списка зависит от хеш-значений его элементов, поэтому оно изменяется при изменении элементов.

Использование идентификаторов для хэшей списков подразумевает, что все списки сравниваются по-разному, что было бы удивительно и неудобно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...