Чому я не можу використовувати список як ключ dict у python?


102

Я трохи заплутався у тому, що можна, а що не можна використовувати як ключ для диктовки python.

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

Отже, кортеж - це незмінний тип, але якщо я приховую список всередині нього, то це не може бути ключем .. чи не міг я так само легко приховати список всередині модуля?

Я мав якусь туманну ідею, що ключ повинен бути "хешувальним", але я просто збираюся визнати своє власне незнання щодо технічних деталей; Я не знаю, що насправді тут відбувається. Що б пішло не так, якби ви спробували використовувати списки як ключі з хешем як, скажімо, місцем їхньої пам’яті?


1
Ось хороша дискусія: stackoverflow.com/questions/2671211/…
Ернан

49
Посміхнувся з назви вашої змінної.
kindall

Відповіді:


35

Існує хороша стаття на цю тему у вікі-програмі Python: Чому списки не можуть бути ключами словника . Як там пояснено:

Що може піти не так, якби ви спробували використовувати списки як ключі з хешем як, скажімо, місцем їхньої пам’яті?

Це можна зробити, не порушуючи жодних вимог, але це призводить до несподіваної поведінки. Зазвичай списки трактуються так, ніби їх значення було отримано із значень вмісту, наприклад, при перевірці (не-) рівності. Багато хто - зрозуміло - очікують, що ви можете скористатися будь-яким списком, [1, 2]щоб отримати той самий ключ, де вам доведеться зберігати приблизно однаковий об'єкт списку. Але пошук за значенням розривається, як тільки список, що використовується як ключ, модифікується, а для пошуку за ідентифікацією потрібно, щоб ви тримали приблизно той самий список - що не потрібно для будь-якої іншої загальної операції зі списком (принаймні жодного, про що я не можу придумати ).

Інші об’єкти, такі як модулі, objectвсе одно роблять набагато більшу угоду з ідентифікацією своїх об’єктів (коли востаннє у вас були два окремі об’єкти модуля, які називаються sys?), І їх все одно порівнюють. Тому менш дивно - або навіть очікувано - що вони, використовуючись як ключі dict, порівнюються за ідентичністю і в цьому випадку.


31

Чому я не можу використовувати список як ключ dict у python?

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

(для всіх, хто натрапляє на це питання, шукаючи шляхи його обходу)

як пояснюють інші тут, насправді ви не можете. Однак ви можете використовувати його рядкове представлення, якщо ви дійсно хочете використовувати свій список.


6
Вибачте, я насправді не бачу вашої точки зору. Нічим не відрізняється від використання рядкових літералів як ключів.
wim

12
Правда; Я просто побачив стільки відповідей, які насправді пояснюють, чому ви не можете використовувати списки з точки зору "ключ повинен бути хеш", що є настільки правдивим, що я хотів запропонувати спосіб обійти його, на той випадок, якщо хтось (новий) буде шукати його ...
Ремі

5
Чому б просто не перетворити список на кортеж? Навіщо перетворювати його на рядок? Якщо ви використовуєте кортеж, він буде коректно працювати з класами, які мають власний метод порівняння __eq__. Але якщо перетворити їх у рядки, все порівнюється за поданням рядка.
Аран-Фей

хороший момент @ Аран-Фей. Просто переконайтеся, що будь-який елемент кортежу сам по собі хеш. наприклад кортеж ([[1,2], [2,3]]) як ключ не працюватиме, оскільки елементи кортежу все ще є списками.
Ремі

17

Щойно знайшов, ви можете змінити список на кортеж, а потім використовувати його як клавіші.

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

15

Справа в тому, що кортежі незмінні, а списки - ні. Розглянемо наступне

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

Що слід d[li]повернути? Це той самий список? Як щодо d[[1,2,3]]? Він має однакові значення, але це інший список?

Зрештою, задовільної відповіді немає. Наприклад, якщо єдиним ключем, який працює, є оригінальний ключ, то якщо у вас немає посилання на цей ключ, ви більше ніколи не зможете отримати доступ до значення. За допомогою кожного іншого дозволеного ключа ви можете створити ключ без посилання на оригінал.

Якщо обидві мої пропозиції працюють, у вас є дуже різні ключі, які повертають одне і те ж значення, що є більш ніж дивним. Якщо працює лише оригінальний вміст, ваш ключ швидко зіпсується, оскільки списки створені для модифікації.


Так, це той самий список, тому, я очікував d[li]би залишитися 5. d[[1,2,3]], посилався б на інший об’єкт списку як на ключ, тож це була би KeyError. Я насправді не бачу жодної проблеми .. за винятком того, що дозволяючи ключу збирати сміття, це може зробити деякі значення dict недоступними. Але це практична проблема, а не логічна проблема ..
wim

@wim: d[list(li)]помилка KeyError - це частина проблеми. Майже кожен другий прецедент , liбуде не відрізняється від нового списку з однаковим вмістом. Це працює, але багатьом це протиінтуїтивно. Плюс, коли востаннє вам справді доводилось використовувати список як ключ dict? Єдиний випадок використання, який я можу собі уявити, - це коли ви все одно перемішуєте все за ідентичністю, і в такому випадку вам слід просто зробити це, замість того, щоб покладатися на __hash__і __eq__бути заснованим на ідентичності.

@delnan Чи проблема просто в тому, що через такі ускладнення було б не надто корисним диктом? чи є якась причина, чому це могло насправді порушити дикт?
wim

1
@wim: Останнє. Як зазначено у моїй відповіді, це насправді не порушує вимог до ключів dict, але, швидше за все, це спричинить більше проблем, ніж вирішує.

1
@delnan - ти хотів сказати "колишній"
Джейсон,

9

Ось відповідь http://wiki.python.org/moin/DictionaryKeys

Що може піти не так, якби ви спробували використовувати списки як ключі з хешем як, скажімо, місцем їхньої пам’яті?

Пошук різних списків з однаковим вмістом дасть різні результати, навіть якщо порівняння списків з однаковим вмістом означатиме їх рівноцінними.

А як щодо використання літералу списку при пошуку словника?


4

Оскільки списки можна змінювати, 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його більше не можна знайти в дикті, оскільки хеш змінився. Знаходить лише лінійне сканування списку ключів дикту 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

Здається, справи працюють, як очікувалося, але подумайте, що відбувається: коли всі екземпляри вашого класу видають одне і те ж хеш-значення, ви матимете колізійне зіткнення, коли в a dictабо в присутності буде більше двох екземплярів як ключі set.

Щоб знайти правильний екземпляр з my_dict[key]або 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ключі, тому що кортежі незмінні та розмиваються.


>>> x = (1,2,3321321321321,) >>> id (x) 139936535758888 >>> z = (1,2,3321321321321,) >>> id (z) 139936535760544 >>> id ((1, 2,3321321321321,)) 139936535810768 Ці 3 мають однакові значення кортежу, але різні ідентифікатори. Отже, словник із ключем x не матиме значення для ключа z?
Ашвані

@Ashwani ти спробував?
timgeb

Так, це працює, як очікувалось, я сумніваюся, що всі кортежі з однаковими значеннями мають різні ідентифікатори. Отже, на основі чого обчислюється цей хеш?
Ашвані

@Ashwani Хеш xі zє однаковим. Якщо щось у цьому незрозуміле, відкрийте нове запитання.
timgeb

1
@Ashwani hash(x)та hash(z).
timgeb

3

Ваш оглядач можна знайти тут:

Чому списки не можуть бути ключами словника

Новачки Python часто дивуються, чому, хоча мова включає як кортеж, так і тип списку, кортежі можна використовувати як словникові ключі, тоді як списки - ні. Це було навмисне дизайнерське рішення, і найкраще це можна пояснити, попередньо зрозумівши, як працюють словники Python.

Джерело та додаткова інформація: http://wiki.python.org/moin/DictionaryKeys


1

Проста відповідь на ваше запитання полягає в тому, що список класів не реалізує хеш методу, який необхідний для будь-якого об'єкта, який бажає використовуватись як ключ у словнику. Однак причина, чому хеш не реалізований так само, як це відбувається, скажімо, клас кортежу (на основі вмісту контейнера) полягає в тому, що список можна змінювати, тому для редагування списку потрібно буде перерахувати хеш, що може означати список у тепер знаходиться в неправильному відрі всередині хеш-таблиці underling. Зверніть увагу, що оскільки ви не можете змінити кортеж (незмінний), ця проблема не виникає.

Як зауваження, фактична реалізація пошуку диктобектів базується на алгоритмі D від Knuth Vol. 3, розділ 6.4. Якщо у вас є ця книга доступна для вас, це може бути варто прочитати, крім того, якщо вам справді дуже цікаво, ви можете заглянути в коментарі розробника щодо фактичної реалізації dictobject тут. Це детально описує, як саме це працює. Також є лекція на python про впровадження словників, які можуть вас зацікавити. Вони проходять визначення ключа та те, що таке хеш у перші кілька хвилин.


-1

Відповідно до документації Python 2.7.2:

Об'єкт можна хешувати, якщо він має хеш-значення, яке ніколи не змінюється протягом свого життя (йому потрібен метод hash ()), і його можна порівняти з іншими об'єктами (йому потрібен метод eq () або cmp ()). Хеш-об'єкти, які порівнюють рівні, повинні мати однакове хеш-значення.

Хашируемость робить об’єкт придатним для використання в якості ключа словника та елемента набору, оскільки ці структури даних використовують хеш-значення всередині.

Усі незмінні вбудовані об'єкти Python можна хешувати, тоді як жодні змінні контейнери (наприклад, списки чи словники) не мають. Об'єкти, які є екземплярами визначених користувачем класів, за замовчуванням хешуються; всі вони порівнюють нерівні, а їх хеш-значення - це їх id ().

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

Використання ідентифікаторів для хеш-списків означатиме, що всі списки по-різному порівнюються, що було б дивно і незручно.


1
Це не відповідає на питання, правда? hash = idне розбиває інваріант в кінці першого абзацу, питання полягає в тому, чому це не зроблено таким чином.

@delnan: Я додав останній абзац для уточнення.
Нікола Мусатті

-1

Словник - це HashMap, він зберігає карту ваших ключів, значення, перетворене на хешоване нове відображення ключів і значень.

щось на зразок (код psuedo):

{key : val}  
hash(key) = val

Якщо вам цікаво, які доступні варіанти можуть бути використані як ключ до вашого словника. Тоді

будь-що, що має хеш (можна перетворити на хеш і утримувати статичне значення, тобто незмінне, щоб зробити хешований ключ, як зазначено вище), є прийнятним, але оскільки об'єкти списку або набору можуть змінюватися в дорозі, тому хеш (ключ) також повинен змінюватись лише для синхронізації зі своїм списком або набором.

Ви можете спробувати :

hash(<your key here>)

Якщо він працює нормально, його можна використовувати як ключ для вашого словника, або ж перетворити його на щось, що хеш.


Коротко :

  1. Перетворити цей список на tuple(<your list>).
  2. Перетворити цей список на str(<your list>).

-1

dictключі повинні бути розмитими. Списки можна змінювати, і вони не забезпечують дійсний хеш- метод.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.