Что такое хэшируемый тип данных
Что проверяют
Понимаете ли вы, почему одни объекты годятся в ключи словаря и элементы множества, а другие нет. Вопрос базовый, но он быстро выводит на контракт хэша и равенства, а его ломают руками чаще всего, когда пишут свой класс с __eq__.
Как отвечать
Объект хэшируем, если у него есть __hash__, возвращающий целое, и это значение не меняется за время жизни объекта.
Ключевое требование это не неизменяемость сама по себе, а стабильность хэша. Просто у неизменяемых объектов она получается бесплатно, поэтому хэшируемы числа, строки, байты, кортежи, frozenset. Списки, словари и set менять можно, значит хэш ехал бы вместе с содержимым, и __hash__ у них снят. Кортеж хэшируем ровно настолько, насколько хэшируемо его содержимое: кортеж со списком внутри бросит TypeError.
Свои классы по умолчанию хэшируемы, хэш берётся от идентичности. Но как только объявлен __eq__, интерпретатор выставляет __hash__ в None, и класс становится нехэшируемым. Чинится так:
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
return (self.x, self.y) == (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y))Контракт формулируйте вслух: равные объекты обязаны иметь равный хэш, обратное неверно.
Где спотыкаются
Отвечают, что хэшируемое значит неизменяемое, и садятся на встречном вопросе про обычный объект, который меняется, но прекрасно работает ключом.
Считают хэш по изменяемым полям, а потом меняют поле у объекта, уже лежащего в множестве. Объект теряется: искать его будут в другом бакете, и проверка вхождения вернёт ложь при физически присутствующем элементе.
Считают хэш строки постоянным между запусками. Он солится случайным значением на старте процесса, поэтому на него нельзя опираться в ключах внешнего кэша и в раскладке данных по шардам.