Ключи словаря

Наверняка вы обратили внимание, что во всех примерах в качестве ключей элементов использовались только неизменяемые объекты: строки и один кортеж. На самом деле в качестве ключей элементов могут использоваться любые хешируемые объекты - объекты которые с момента появления в программе и до их утилизации не меняют свой хешь (уникальный идентификатор). К таким объектам относятся: числа (int, float и complex), строки (str, bytes), замороженные множества (frozenset) и кортежи (tuple). Но с кортежами есть один важный нюанс. Объекты всех перечисленных типов данных обладают специальным методом .__hash__(), например:

>>> a = 1
>>> a.__hash__()
1
>>>
>>> b = 'ABC'
>>> b.__hash__()
1976167791

Как мы знаем, числа и строки являются неизменяемыми, поэтому если мы попытаемся изменить данные объекты через переменные a и b то это на самом деле приведет к тому что появятся новые объекты:

>>> a += 1
>>> a.__hash__()
2
>>>
>>> b += 'D'
>>> b.__hash__()
-228539795

Как видите, число \(1\) и строка 'ABC' оказались не просто неизменяемыми, но еще и хешируемыми. Да, кортежи относятся к неизменяемому типу данных, но только до той поры, пока они сами состоят из неизменяемых объектов, например:

>>> a = [1, 2, 3]     # список - это изменяемый объект
>>> a[0] = 111    # может быть изменен "на месте"
>>> a
[111, 2, 3]
>>>
>>> b = (1, 2, 3)     # кортеж - это неизменяемый объект
>>> b[0] = 111
TypeError: 'tuple' object does not support item assignment
>>>
>>>
>>> b.__hash__()    # но у кортежа есть хеш
-2022708474
>>>
>>> a.__hash__()    # а у списка нет
TypeError: 'NoneType' object is not callable

Мы не можем менять кортежи так же как мы меняем списки, т.е. что бы изменить кортеж (1, 2, 3) на (111, 2, 3) нам придется создать новый объект:

>>> b.__hash__()
-2022708474
>>>
>>> b = (111,) + b[1:]
>>> b
(111, 2, 3)
>>>
>>> b.__hash__()
-1576498621

Но что, если одним из элементов кортежа является изменяемый объект? Давайте проверим:

>>> c = ('a', [0, 1])
>>>
>>> c[1][0] = -9999
>>>
>>> c
('a', [-9999, 1])

Оказывается, кортежи невсегда являются неизменяемыми, а значит они и невсегда являются хешируемыми:

>>> c.__hash__()
TypeError: unhashable type: 'list'

Следовательно и ключами значений словаря они так же могут быть только в том случае, если сами состоят из неизменяемых элементов:

>>> # ключи-кортежи неизменяемы и хешируемы:
>>> a = {('W', 'Q'): 123, (1, 2): 'abc'}
>>> a
{('W', 'Q'): 123, (1, 2): 'abc'}
>>>
>>>
>>> # ключи-кортежи изменяемы и нехешируемы:
>>> a = {('W', [0, 1]): 123, (1, ['a', 'b']): 'abc'}
TypeError: unhashable type: 'list'

Еще, на всякий пожарный, следует отметить что в качестве ключей элементов не стоит использовать числа типа float, так как из-за двоичной арифметики компьютера могут возникать некоторые неточности, например:

>>> a = {0.1: 15, 0.2: 24, 0.3: 38}
>>> a
{0.1: 15, 0.2: 24, 0.3: 38}
>>>
>>> a[0.1]     # вроде бы все работает
15
>>> a[0.3]     # прекрасно работает
38

Но если попытаться выполнить следующую команду, то мы получим ошибку KeyError:

>>> a[0.1 + 0.1 + 0.1]
KeyError: 0.30000000000000004

Дело в том что некоторые конечные десятичные дроби являются бесконечными в двоичном представлении, а из-за усечений возникают небольшие погрешности:

>>> 0.1+0.1+0.1
0.30000000000000004

По той же самой причине не рекомендуется использовать в качестве ключей и комплексные числа. Но имейте в виду, что это всего лишь рекомендация, ведь прямое обращение по ключу a[0.3] прекрасно работает.