Равенство и хешируемость в Python: ключи словаря и set
Словарь и множество должны быстро находить уже сохранённый объект. Для этого Python сначала использует хеш, а затем подтверждает совпадение проверкой равенства. Два объекта, которые считаются равными, обязаны иметь одинаковый хеш. Обратное неверно: одинаковые хеши возможны у неравных объектов, поэтому __eq__ всё равно необходим. Ошибка в этой договорённости проявляется не только неверным сравнением, но и потерянными ключами, дубликатами в set или поиском, который зависит от момента изменения объекта.
Равенство значения и идентичность
Оператор is проверяет, являются ли две ссылки одним объектом. Оператор == вызывает протокол равенства и может считать разные экземпляры равными по содержимому. Для доменного значения, например координаты, обычно важны компоненты, а не место объекта в памяти. Метод __eq__ может вернуть NotImplemented, если не умеет сравнивать объект с данным типом: тогда Python получает возможность попробовать отражённое сравнение.
Требования к хешируемому объекту
Хешируемость предполагает стабильный hash(obj) на протяжении присутствия объекта в словаре или множестве. Поэтому поля, участвующие в __eq__ и __hash__, не должны меняться. Если класс определяет равенство, но не безопасный хеш, Python обычно делает экземпляры нехешируемыми. Это полезная защита: изменяемое значение подходит элементу списка, но опасно в роли ключа.
Рабочий пример неизменяемого ключа
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Point:
x: int
y: int
first = Point(2, 5)
second = Point(2, 5)
labels = {first: 'склад'}
unique = {first, second}
print(first is second)
print(first == second)
print(labels[second])
print(len(unique))Ожидаемый результат
False
True
склад
1Два экземпляра различны по идентичности, но равны как координаты. Для frozen=True dataclass генерирует согласованные __eq__ и __hash__ из полей, поэтому словарь находит значение через второй экземпляр, а множество хранит одну эквивалентную точку. Обычное присваивание first.x = 3 завершится FrozenInstanceError, и ключ не изменит хеш во время хранения.
Как выявить несогласованные равенство и хеш
Распространённая ошибка — определить __eq__, а затем попытаться добавить экземпляр в множество. Python сообщает TypeError: unhashable type: 'Point'. Не исправляйте это механическим __hash__ = object.__hash__: хеш по идентичности противоречит равенству по координатам. Сначала решите, является ли объект неизменяемым значением. Если да, вычисляйте хеш из тех же компонентов; если нет, не используйте его как ключ.
Ещё опаснее изменить поле после вставки. Элемент окажется в корзине, соответствующей старому хешу, и поиск может перестать его находить. Для диагностики сравните hash(obj) до и после предполагаемого изменения и проверьте все поля, задействованные в равенстве.
Практикум: хешируемый идентификатор книги
Создайте неизменяемый по публичному интерфейсу класс BookId с полями isbn и edition. Реализуйте равенство и хеш через кортеж этих значений. Сложите два равных экземпляра в set и используйте один как ключ, а вторым выполните поиск. Самостоятельная проверка: равные идентификаторы дают одинаковый хеш и одну запись в множестве; другой номер издания остаётся отдельным элементом; сравнение со строкой возвращает False, а не выбрасывает случайную ошибку доступа к атрибуту.
Инварианты равенства и хеша
Идентичность и равенство отвечают на разные вопросы. Правило a == b означает hash(a) == hash(b) для хешируемых объектов. Хеш строят по тем же неизменяемым данным, что и равенство. Изменяемый объект с содержательным __eq__ безопаснее оставить нехешируемым.
Вопросы об идентичности и хешах
Должны ли разные объекты иметь разные хеши?
Нет. Коллизии допустимы и разрешаются сравнением равенства. Требуется только одинаковый хеш у равных объектов.
Почему список нельзя сделать ключом словаря?
Содержимое списка меняется, а значит его значение и потенциальный хеш нестабильны. Для ключа часто подходит кортеж из хешируемых элементов.
Когда сравнивать через is?
is используют для проверки идентичности, особенно с единственным объектом None. Для сравнения доменных значений предназначен ==.