Какие есть ограничения на ключи в dict в Python?
Что проверяют
Понимаете ли вы, что ключ обязан быть хэшируемым, а не просто неизменяемым, и как это связано с устройством хэш-таблицы. Следом часто спрашивают, почему True и 1 оказываются одним ключом и можно ли сделать ключом свой объект.
Как отвечать
Сформулируйте точное правило: у ключа есть __hash__, который не меняется за время жизни объекта, и согласованный с ним __eq__: если a == b, то hash(a) == hash(b). Словарь ищет ключ по хэшу, а затем сравнивает кандидатов по идентичности и равенству. Отсюда следствия:
- встроенные типы:
int,float,str,bytes,frozenset,Noneхэшируются, аlist,dict,setнет, попытка даётTypeError: unhashable type; - кортеж: хэшируется, только если хэшируемы все элементы,
(1, [2])ключом не станет; - равные значения сливаются:
1,1.0иTrueэто один ключ,{1: "a", True: "b"}даст словарь из одной пары; - NaN:
float("nan")не равен сам себе, поэтому новый объект NaN старый ключ не найдёт, а тот же самый объект найдёт, ведь сначала проверяется идентичность; - свои классы: по умолчанию хэшируются по
id. Если переопределить__eq__без__hash__, Python выставит__hash__ = None, и объект станет нехэшируемым.@dataclass(frozen=True)генерирует оба метода сам.
Для ML-кода важны частные случаи: numpy.ndarray, DataFrame и Series ключами быть не могут, а скаляры вроде np.int64(1) хэшируются как обычный int и совпадают с ним. Чтобы закэшировать результат по массиву, берут arr.tobytes() или кортеж, а для ключа по нескольким колонкам кортеж из их значений.
Где спотыкаются
Говорят «ключ должен быть неизменяемым». Кортеж неизменяем, но со списком внутри не хэшируется, а изменяемый объект своего класса хэшируется по id без проблем.
Делают ключом объект с хэшем от изменяемых полей и меняют его после вставки. Словарь не падает, но ключ больше не находится ни по старому, ни по новому значению.
Не знают, что 1 и True сливаются. На этом легко потерять данные, когда маппинг строится по смешанной колонке, где встречаются и булевы, и числовые метки.