Когда объект, сравнимый по значению, всё равно нельзя использовать ключом словаря?
Объект нельзя использовать ключом словаря, если он нехешируемый: Python не может получить для него стабильное хеш-значение. Одной поддержки сравнения недостаточно: для ключа требуется согласованная пара __hash__ и __eq__.
Если два объекта равны, их хеши обязаны совпадать. Хеш также не должен изменяться в течение времени, пока объект используется ключом словаря.
Словари реализуют отображение на основе хеш-таблиц. Хеш позволяет быстро определить предполагаемую область хранения ключа, а сравнение используется для проверки точного совпадения при коллизиях.
Такой подход отделяет быстрый предварительный поиск по хешу от проверки равенства объектов. Поэтому Python предъявляет к ключам более строгие требования, чем просто возможность сравнивать их между собой.
Списки, словари и множества обычно нельзя использовать ключами: их содержимое изменяемо, а значит, стабильный хеш для них не гарантируется. Попытка добавить такой объект в словарь приводит к TypeError.
Особенно опасен пользовательский класс, который формально разрешает хеширование, но вычисляет хеш по изменяемым полям. После изменения такого поля словарь может сохранить запись в старом месте, а поиск по тому же объекту начнёт использовать другое место.
При обращении к словарю Python сначала вычисляет hash(key). Затем среди объектов с подходящим хешем выполняется сравнение ключей через равенство. Совпадение хешей само по себе не доказывает равенство: коллизии допустимы.
Обязательное правило имеет только одно направление: если a == b, то hash(a) == hash(b). Обратное неверно: одинаковый хеш могут иметь неравные объекты.
Класс, определяющий __eq__, но не задающий корректный __hash__, обычно становится нехешируемым: Python устанавливает для него __hash__ в None. Это защищает от нарушения контракта ключей.
Хешируемость составных объектов проверяется рекурсивно. Например, кортеж может быть ключом только тогда, когда хешируем каждый его элемент; кортеж со списком внутри ключом быть не может.
Минимальный пример:
Сравнение двух объектов в примере возвращает True, но вызов hash(user) приводит к TypeError, потому что класс переопределил __eq__ и не предоставил совместимый __hash__.
Практически безопасными ключами часто делают неизменяемые значения: строки, числа, кортежи из хешируемых элементов или frozenset. Если равенство и хеш основаны на нескольких полях, эти поля нельзя менять во время использования объекта в словаре или множестве.
Сервис кэширует результаты по фильтрам запроса. Передавать словарь фильтров напрямую как ключ нельзя: словарь нехешируем и к тому же изменяем.
Можно сериализовать фильтры в строку. Это удобно для кэша и логирования, но требует однозначной сериализации: порядок полей, типы значений и специальные значения должны обрабатываться согласованно.
Другой вариант — нормализовать данные в кортеж пар ключ-значение, где пары и значения хешируемы. Он лучше явно отражает структуру ключа, но требует дополнительной нормализации вложенных списков и словарей.
Выбор кортежа нормализованных неизменяемых значений обычно предпочтителен: он сохраняет семантику данных, не зависит от случайного формата строки и обеспечивает стабильный ключ. Изменение исходного словаря после создания ключа уже не влияет на сформированное значение кэша.
Да. Это называется коллизией и является нормальной ситуацией для хеш-таблицы. Хеш имеет ограниченный размер, поэтому разных объектов потенциально больше, чем возможных хеш-значений. Словарь разрешает коллизии и дополнительно проверяет равенство ключей.
Хеш кортежа вычисляется с учётом его элементов. Если хотя бы один элемент нехешируем, Python не может вычислить стабильный хеш всего кортежа. Поэтому кортеж со строками может быть ключом, а кортеж со списком — нет.
Само изменение допустимо технически, но оно опасно, если изменённое состояние участвует в __hash__ или __eq__. Словарь оставит запись в позиции, рассчитанной по старому хешу, а последующий поиск может использовать новый хеш и не найти существующий ключ. Надёжная реализация делает ключ логически неизменяемым либо вычисляет хеш только по неизменяемому состоянию.