← Назад к вопросам

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

1.8 Middle🔥 161 комментариев
#Python Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Краткое резюме

Неизменяемые типы (Immutable) — это типы, содержимое которых нельзя менять после создания (строка, кортеж, число). Хэшируемые типы (Hashable) — это типы, для которых можно вычислить hash-код и использовать как ключи словаря или элементы множества. Все неизменяемые встроенные типы хэшируемы, но не все хэшируемые типы неизменяемы. Это разные концепции, хотя часто они совпадают.

Неизменяемые типы (Immutable)

Определение: Тип, содержимое которого нельзя менять после создания.

Встроенные неизменяемые типы Python

# Неизменяемые
my_int = 42
my_float = 3.14
my_str = "hello"
my_tuple = (1, 2, 3)
my_frozenset = frozenset([1, 2, 3])
my_bytes = b"hello"

# Попытка изменить
# my_str[0] = "H"  # TypeError
# my_tuple[0] = 999  # TypeError
# my_frozenset.add(4)  # AttributeError

Характеристики

# Неизменяемость означает, что операции создают новые объекты
str1 = "hello"
str2 = str1.upper()  # "HELLO" — новый объект
print(id(str1) == id(str2))  # False

str1 += " world"  # Кажется, что меняем, но создаём новый объект
print(id(str1))  # Другой id — новая строка

# То же с кортежами
tuple1 = (1, 2)
tuple2 = tuple1 + (3,)  # Новый кортеж
print(id(tuple1) == id(tuple2))  # False

Хэшируемые типы (Hashable)

Определение: Тип, для объектов которого можно вычислить hash-функцию. Хэшируемые объекты можно использовать как ключи словаря или элементы set.

Встроенные хэшируемые типы

# Хэшируемые встроенные типы
print(hash(42))  # 42
print(hash(3.14))  # 7323220122232254 (или другое)
print(hash("hello"))  # -1234567890123 (зависит от версии)
print(hash((1, 2, 3)))  # 529344067295497451
print(hash(frozenset([1, 2])))  # 8765432123
print(hash(True))  # 1
print(hash(None))  # 8765432100

# Нехэшируемые типы
# hash([1, 2, 3])  # TypeError: unhashable type: 'list'
# hash({"a": 1})  # TypeError: unhashable type: 'dict'
# hash({1, 2})  # TypeError: unhashable type: 'set'

Использование в словарях

# Хэшируемые — можно как ключи
dict1 = {
    1: "int",
    "key": "string",
    (1, 2): "tuple",
    frozenset([3, 4]): "frozenset",
    True: "bool"
}
print(dict1)  # Работает!

# Нехэшируемые — нельзя как ключи
# dict2 = {
#     [1, 2]: "list",  # TypeError
#     {"a": 1}: "dict",  # TypeError
#     {1, 2}: "set"  # TypeError
# }

Использование в множествах

# Хэшируемые — можно добавлять в set
set1 = {1, "hello", (1, 2), frozenset([3, 4])}
print(set1)  # Работает!

# Нехэшируемые — нельзя
# set2 = {1, [2, 3]}  # TypeError: unhashable type: 'list'
# set3 = {1, {"a": 1}}  # TypeError: unhashable type: 'dict'

Разница: Immutable ≠ Hashable

1. Кастомные типы

Можно создать неизменяемый, но нехэшируемый тип:

from typing import Tuple

class ImmutableButNotHashable:
    """Неизменяемый класс, но нехэшируемый (нет __hash__)"""
    
    def __init__(self, name: str):
        self._name = name
    
    @property
    def name(self) -> str:
        return self._name
    
    def __eq__(self, other):
        if not isinstance(other, ImmutableButNotHashable):
            return False
        return self._name == other._name
    
    # Явно нельзя хэшировать
    __hash__ = None

obj = ImmutableButNotHashable("John")
# obj._name = "Jane"  # Не может быть изменено (нет setter)
# hash(obj)  # TypeError: unhashable type
# {obj: "value"}  # TypeError: unhashable type

2. Хэшируемый, но изменяемый (редко)

Можно создать хэшируемый, но изменяемый тип (плохая идея!):

class HashableButMutable:
    """Хэшируемый, но изменяемый класс (ПЛОХАЯ ИДЕЯ!)"""
    
    def __init__(self, value: int):
        self.value = value  # Может изменяться
    
    def __hash__(self):
        return hash(self.value)
    
    def __eq__(self, other):
        if not isinstance(other, HashableButMutable):
            return False
        return self.value == other.value

obj1 = HashableButMutable(42)
obj2 = HashableButMutable(42)

# Добавляем в словарь
dict1 = {obj1: "first"}
print(dict1)  # {<HashableButMutable>: "first"}

# Меняем объект
obj1.value = 100

# Теперь hash изменился, словарь сломан!
print(dict1)  # Ключ всё ещё в словаре, но hash другой
print(dict1.get(obj1))  # None!
print(obj1 in dict1)  # False (хотя объект там есть)

# Это приводит к ошибкам и утечкам памяти

Почему это важно

1. Идентификация в словарях

Для работы в словарях нужна стабильность hash:

class User:
    def __init__(self, id: int, name: str):
        self.id = id
        self.name = name  # Может изменяться
    
    def __hash__(self):
        return hash(self.id)
    
    def __eq__(self, other):
        return isinstance(other, User) and self.id == other.id

users_dict = {}
user = User(1, "Alice")
users_dict[user] = "some_data"

# Меняем name (не влияет на hash, так как hash от id)
user.name = "Bob"
print(users_dict[user])  # "some_data" — работает

# Но если менять id — словарь сломается!
user.id = 2
print(users_dict.get(user))  # None — ключ потерялся!

2. Производительность кэширования

from functools import lru_cache

# Работает с неизменяемыми аргументами
@lru_cache(maxsize=128)
def expensive_operation(data: tuple):
    # Долгая операция
    return sum(data) ** 2

result1 = expensive_operation((1, 2, 3))  # Вычисляет
result2 = expensive_operation((1, 2, 3))  # Берёт из кэша
print(result1 == result2)  # True

# Не работает со списками
# @lru_cache(maxsize=128)
# def expensive_operation(data: list):  # TypeError: unhashable type
#     return sum(data) ** 2

Таблица: Immutable vs Hashable

ТипImmutableHashableКлюч dictВ set
intДаДа
floatДаДа
strДаДа
tupleДаДа (если элементы hashable)
frozensetДаДа
bytesДаДа
NoneДаДа
boolДаДа
listНетНет
dictНетНет
setНетНет

Кортежи с изменяемыми элементами

Важный случай: Кортеж неизменяем, но если содержит изменяемые элементы — он нехэшируем!

# Кортеж с неизменяемыми элементами — хэшируемый
tuple1 = (1, 2, "hello")
print(hash(tuple1))  # 529344067295497451
print({tuple1: "key"})  # Работает!

# Кортеж с изменяемыми элементами — нехэшируемый
tuple2 = (1, [2, 3], "hello")
# hash(tuple2)  # TypeError: unhashable type: 'list'
# {tuple2: "key"}  # TypeError

# Почему? Потому что hash зависит от содержимого
# Если список внутри изменится, hash изменится

Проверка hashability

def is_hashable(obj) -> bool:
    """Проверить, хэшируемый ли объект"""
    try:
        hash(obj)
        return True
    except TypeError:
        return False

print(is_hashable(42))  # True
print(is_hashable("hello"))  # True
print(is_hashable((1, 2)))  # True
print(is_hashable([1, 2]))  # False
print(is_hashable({"a": 1}))  # False

# Или проверить через __hash__
print(hasattr(int, "__hash__"))  # True
print(hasattr(list, "__hash__"))  # False (или None)

Кастомные классы

from dataclasses import dataclass

# По умолчанию кастомные классы хэшируемы
class User:
    def __init__(self, id: int):
        self.id = id

user1 = User(1)
user2 = User(1)
print(hash(user1) != hash(user2))  # True (разные объекты, разные hash)
print({user1: "data"})  # Работает

# Но если добавить __eq__, нужно добавить и __hash__
class UserWithEq:
    def __init__(self, id: int):
        self.id = id
    
    def __eq__(self, other):
        return isinstance(other, UserWithEq) and self.id == other.id
    
    # Если есть __eq__, нужно явно определить __hash__
    def __hash__(self):
        return hash(self.id)

user1 = UserWithEq(1)
user2 = UserWithEq(1)
print(hash(user1) == hash(user2))  # True
print({user1: "data"}[user2])  # "data" — работает!

# С dataclass
@dataclass(frozen=True)  # frozen=True → неизменяемо и хэшируемо
class Product:
    id: int
    name: str

product = Product(1, "Laptop")
print(hash(product))  # Работает
print({product: "stock: 10"})  # Работает

Лучшие практики

  1. Используй неизменяемые типы для ключей и элементов set: tuple, frozenset, str, int, bytes
  2. Если нужен кастомный тип как ключ: сделай его неизменяемым и определи hash и eq
  3. Избегай изменяемых элементов в кортежах, которые хэшируешь
  4. Не меняй hash-код объекта во время его использования как ключ: это сломает словарь
  5. Если класс имеет eq, всегда определи hash (или явно запрети хэширование)
  6. Для нехэшируемых объектов используй list вместо dict/set

Заключение

  • Immutable = не меняется после создания
  • Hashable = можно использовать как ключ/элемент в set
  • Обычно они совпадают, но это не обязательно
  • Все встроенные неизменяемые типы хэшируемы, обратное неверно
  • Для production кода важна оба: неизменяемость для безопасности, хэшируемость для эффективности
В чем разница между неизменяемыми и хэшируемыми типами данных? | PrepBro