При совпадении хешей у двух ключей словаря как Python определяет, занимают ли они одну запись?
Совпадение хешей само по себе не означает, что ключи одинаковы. Python дополнительно сравнивает ключи на равенство: если они равны, используется одна запись и новое значение заменяет старое; если не равны, записи остаются разными.
Словари основаны на хеш-таблицах, которые обеспечивают быстрый доступ к данным по ключу. Разные ключи могут иметь одинаковый хеш, поэтому хеширование должно использоваться только как быстрый предварительный фильтр, а не как окончательная проверка равенства.
Если считать ключи одинаковыми только при совпадении хешей, различные объекты начнут ошибочно перезаписывать данные друг друга. Если же игнорировать равенство после проверки хеша, поиск существующего ключа перестанет соответствовать обычной семантике операторов сравнения.
Ключ также обязан сохранять согласованность между хешем и равенством: если два ключа равны, их хеши должны совпадать. Обратное неверно: одинаковый хеш не требует равенства ключей.
При добавлении или поиске ключа словарь вычисляет его хеш и использует хеш-таблицу для выбора кандидатов. При обнаружении того же хеш-значения Python проверяет равенство ключей. Равные ключи обращаются к одной логической записи, а неравные размещаются в разных позициях таблицы.
В CPython для разрешения коллизий используется открытая адресация с алгоритмом поиска свободной позиции. Детали размещения и последовательности проверок относятся к реализации и не должны использоваться как часть переносимого контракта Python.
Результат — 2 обновлено второе. У всех объектов один хеш, но first и second не равны, поэтому это разные ключи. Объект again равен first, поэтому обновляет значение уже существующей записи.
Изменяемый объект опасно использовать ключом, если изменение его состояния влияет на __eq__ или __hash__: после изменения словарь может оказаться не в состоянии корректно найти такой ключ. Поэтому ключи обычно делают неизменяемыми или обеспечивают неизменность значимых для сравнения полей.
В системе обрабатываются составные идентификаторы заказов. Разработчик создал класс идентификатора, но в __hash__ вернул константу, полагая, что это сделает ключи одинаковыми. Функционально словарь продолжит работать, если __eq__ реализован правильно, однако большое число коллизий ухудшит производительность поиска.
Первый вариант — оставить константный хеш. Его плюс — простота, но минус — потенциально линейное замедление операций на больших наборах ключей. Второй вариант — удалить собственную реализацию и использовать хеш неизменяемого кортежа значимых полей; это обычно даёт более равномерное распределение и сохраняет согласованность с равенством.
Выбран второй вариант: __eq__ сравнивает идентичность заказа по компонентам, а __hash__ строится по тому же набору компонентов. В результате равные идентификаторы используют одну запись, разные — не перезаписывают друг друга, а обычные операции словаря сохраняют ожидаемую производительность.
Да. Это нормальная коллизия, неизбежная из-за конечного диапазона хеш-значений. Словарь различает такие ключи последующей проверкой равенства, поэтому одинаковый хеш не является нарушением контракта.
Это нарушение контракта __hash__ и __eq__. Словарь может искать равный ключ в другой области таблицы и не найти существующую запись, поэтому пользовательский класс должен гарантировать: если a == b, то hash(a) == hash(b).
Сам объект остаётся внутри таблицы на прежней позиции, но новый хеш может направить поиск в другую позицию. В результате ключ иногда невозможно найти обычным обращением или удалить, хотя он физически присутствует в словаре. Практическое правило — не изменять состояние, участвующее в __eq__ и __hash__, пока объект используется как ключ.