Недавно мы с подписчиками моего ТГ-канала спорили по поводу, казалось бы, совсем простого вопроса – элементы каких типов данных могут быть ключами словаря. Во многих статьях в сети написано, что это объект любого неизменяемого типа данных, например, числа, строки, кортежа. Т.е. ключ словаря – всегда объект неизменяемого типа данных. Но так ли это? А что насчет ключей в виде объектов пользовательских именных функций или экземпляров пользовательских классов. Конечно, мы вряд ли будем создавать такие ключи, но все же их можно использовать или нет?
Проверим:
class MyClass:
pass
obj_1 = MyClass()
obj_2 = MyClass()
dct = {obj_1: 1, obj_2: 2}
print(dct[obj_1])
def func():
pass
dct = {func: 1}
print(dct[func])
Ошибок нет. Получается, что может экземпляр класса и объект пользовательской функции быть ключом! Но ведь это изменяемые типы данных. И это несложно проверить. Если объект относится к изменяемому типу данных, то при попытке изменения id объекта должен остаться прежним.
Вот числа, они неизменяемые:
x = 10
y = x
print(id(x), id(y))
x = x + 1
print(id(x), id(y))
Результат:

При «изменении» значения переменной x мы получили совсем новый объект.
Теперь то же самое повторим на списках:
x = [1, 2, 3]
y = x
print(id(x), id(y))
x.append(4)
print(id(x), id(y))
Результат:

Идентификатор не изменился, т.е. имеем объект изменяемого типа данных.
Теперь посмотрим, что с изменяемостью у экземпляров пользовательских классов:
class User:
def __init__(self, age):
self.age = age
u = User(12)
b = u
print(id(u), id(b))
u.age = 40
print(id(u), id(b))
print(b.age)
Результат:

Получается экземпляры пользовательских классов – изменяемые.
А что с объектами функций? Можем мы у объекта существующей функции изменить логику программным путем? Пример ниже показывает, что можем, т.к. id объекта функции не изменился:
def func():
return "hello"
print(id(func))
def other(a, b):
return a + b
func.__code__ = other.__code__
print(id(func))
Результат:

И это все правильно. Объекты изменяемых типов данных при изменениях сохраняют свой id. Об этом указано в документации.
Значит изменяемость здесь совсем не при чем? В чем же тогда дело и как понять объекты каких классов могут быть ключами словаря? Обратимся к теории.
Словарь, это хеш-таблица, т.е. ключи должны быть объектами классов, у которых существует метод hash. В Python пользовательские функции и классы по умолчанию хешируются, а значит в качестве ключей использоваться могут, но при этом они изменяемые.
P.S. Теперь нужно понять, почему зачастую на интернет-ресурсах про ключи говорят как про объекты неизменяемых типов данных, хотя нужно делать упор не на изменяемость, а на хешируемость. А что вы думаете об этом?