Будет ли tuple хэшируемым, если в него положить список, и почему
Что проверяют
Понимаете ли вы разницу между неизменяемостью контейнера и неизменяемостью того, что в нём лежит. Следом часто просят объяснить, почему ключ словаря обязан быть хэшируемым и что произойдёт при t[1] += [4].
Как отвечать
Нет. Сам кортеж создать можно, но hash((1, [2, 3])) падает с TypeError: unhashable type: 'list'. Хэш кортежа вычисляется из хэшей его элементов в момент вызова, а у list атрибут __hash__ равен None.
Логика такая:
- неизменяемость кортежа поверхностная: он хранит фиксированный набор ссылок, заменить элемент нельзя, но объект по ссылке менять можно,
t[1].append(4)отработает; - контракт хэша: равные объекты обязаны иметь равный хэш, и хэш не должен меняться, пока объект лежит в
dictилиset. Список с меняющимся содержимым этот контракт нарушил бы, поэтому Python запрещает его хэшировать; - следствие: такой кортеж нельзя сделать ключом словаря, элементом множества или аргументом функции под
functools.lru_cache; - что делать: если нужна хэшируемая версия, заменить список на
tupleилиfrozenset, например(1, tuple(lst)).
Хорошо дополнить ответ пользовательскими классами. По умолчанию объект хэшируется по идентичности; если вы определили __eq__ без __hash__, Python выставит __hash__ = None. @dataclass(frozen=True) сгенерирует хэш, но с полем-списком упадёт тем же TypeError при первом вызове hash().
Где спотыкаются
Отвечают «да, ведь tuple неизменяемый». Неизменяемость кортежа не распространяется на вложенные объекты, а хэшируемость проверяется по элементам.
Проверяют через isinstance(t, collections.abc.Hashable) и получают True. Эта проверка смотрит только на наличие __hash__ у типа, надёжный способ один: вызвать hash() внутри try.
Не знают про ловушку t[1] += [4]: список внутри успешно расширяется, а затем присваивание элементу кортежа бросает TypeError. Операция одновременно сработала и упала.