В чем разница между неизменяемыми и хэшируемыми типами данных

Ответ

Неизменяемость (immutability) и хэшируемость (hashability) — это разные, но часто связанные свойства объектов в Python.

  • Неизменяемый объект — это объект, состояние которого нельзя изменить после создания. При любой "операции изменения" создается новый объект. Примеры: int, float, str, tuple, frozenset.
  • Хэшируемый объект — это объект, который имеет хэш-значение (целое число), неизменяемое на протяжении его жизни, и может корректно сравниваться с другими объектами (имеет метод __eq__). Хэшируемые объекты могут быть ключами в словарях (dict) и элементами множеств (set).

Ключевая связь и различие:

  1. Почти все встроенные неизменяемые типы хэшируемы.
  2. Изменяемые типы (списки list, словари dict, множества set) — нехэшируемы.
  3. Хэшируемость не гарантирует неизменяемость. Можно создать пользовательский изменяемый класс и определить для него метод __hash__, но это считается антипаттерном, так как изменение объекта после помещения в словарь нарушит структуру данных.

Примеры:

# 1. Неизменяемый и хэшируемый объект (строка)
my_string = "hello"
my_dict = {my_string: "value"}  # Допустимо, строка хэшируема
print(hash(my_string))  # Выведет хэш-значение

# 2. Изменяемый и нехэшируемый объект (список)
my_list = [1, 2, 3]
try:
    my_dict = {my_list: "value"}  # Вызовет TypeError
except TypeError as e:
    print(e)  # unhashable type: 'list'

# 3. Кортеж (tuple) хэшируем, только если все его элементы хэшируемы.
tuple_hashable = (1, "two", 3.0)
print(hash(tuple_hashable))  # Работает

tuple_unhashable = (1, [2, 3])  # Содержит изменяемый список
try:
    hash(tuple_unhashable)     # Вызовет TypeError
except TypeError as e:
    print(e)  # unhashable type: 'list'

Для тестирования важно понимать эти различия, например, при использовании фикстур pytest или данных в параметризованных тестах, где в качестве ключей параметризации могут использоваться только хэшируемые значения.

Ответ 18+ 🔞

А, ну слушай, вот есть такая тема, которая мозги начинающим выносит напрочь. Неизменяемость и хэшируемость. Это, ёпта, как близнецы, но не братья — похожи, а суть разная. Сейчас разжуем.

Неизменяемый объект (immutable) — это как памятник, блядь. Поставили и всё. Тронуть нельзя. Ну то есть если ты попытаешься его «изменить», тебе просто новый памятник отольют, а старый так и будет стоять. int, str, tuple — они такие, заскорузлые. Создали и забыли.

Хэшируемый объект (hashable) — это уже про другое. Это объект, которому можно выдать паспорт — хэш (это просто число, да). И этот паспорт на всю его жизнь один и тот же. И с таким паспортом его уже можно в особые места пускать — делать ключом в словаре или запихивать в множество (set). Главное правило — паспорт не должен меняться, и личность (значение) должна чётко проверяться (через __eq__).

А теперь, блядь, где собака зарыта:

  1. Почти все встроенные неизменяемые типы — они и хэшируемые. Логично, раз памятник стоит вечно, то и паспорт у него вечный. Это как int, str, tuple (если внутри тоже всё неподвижно, об этом ниже).
  2. Все изменяемые типы (list, dict, set) — нихера не хэшируемые. Ну представь, ты даёшь списку паспорт, а он потом вырастает или сжимается — пиздец, паспорт недействителен, а он уже в словаре ключом записан. Полный бардак, поэтому Python сразу говорит: «Нет, сука, так нельзя».
  3. Вот тут самое интересное: хэшируемость НЕ гарантирует неизменяемость! Технически-то можно взять свой класс, сделать его изменяемым, но написать для него метод __hash__. Но это, ядрёна вошь, чистой воды самоубийство и антипаттерн. Сделаешь так — получишь словарь, который накроется медным тазом, потому что объект изменится, а паспорт-то старый. Доверия к такому коду — ебать ноль.

Примеры, чтобы вообще всё встало на свои места:

# 1. Классика: строка. Неизменяемая и хэшируемая. Всё пучком.
my_string = "hello"
my_dict = {my_string: "value"}  # Можно, паспорт есть
print(hash(my_string))  # Смотри-ка, номер паспорта

# 2. Пиздец и хаос: список. Изменяемый и нехэшируемый.
my_list = [1, 2, 3]
try:
    my_dict = {my_list: "value"}  # Пытаемся впихнуть невпихуемое
except TypeError as e:
    print(e)  # Тебе вежливо скажут: "unhashable type: 'list'". Иди на хуй со своим списком.

# 3. Кортеж — хитрая жопа. Сам по себе неизменяемый, но хэшируемым будет только если ВСЁ внутри него тоже хэшируемо.
tuple_hashable = (1, "two", 3.0)  # Внутри только камни-памятники
print(hash(tuple_hashable))  # Работает, паспорт выдан.

tuple_unhashable = (1, [2, 3])  # А тут внутри сидит живой, шевелящийся список!
try:
    hash(tuple_unhashable)     # Пытаемся выдать паспорт на всю конструкцию
except TypeError as e:
    print(e)  # И опять: "unhashable type: 'list'". Весь кортеж из-за одной сволочи внутри стал нелегалом.

В тестировании это, блядь, важно понимать. Вот в pytest параметризацию делаешь — туда можно передавать в качестве идентификаторов или значений только хэшируемую фигню. Сунешь список — получишь по шапке. Так что держи ухо востро, чувак.