В чем разница между неизменяемыми и хэшируемыми типами данных?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Краткое резюме
Неизменяемые типы (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
| Тип | Immutable | Hashable | Ключ 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"}) # Работает
Лучшие практики
- Используй неизменяемые типы для ключей и элементов set: tuple, frozenset, str, int, bytes
- Если нужен кастомный тип как ключ: сделай его неизменяемым и определи hash и eq
- Избегай изменяемых элементов в кортежах, которые хэшируешь
- Не меняй hash-код объекта во время его использования как ключ: это сломает словарь
- Если класс имеет eq, всегда определи hash (или явно запрети хэширование)
- Для нехэшируемых объектов используй list вместо dict/set
Заключение
- Immutable = не меняется после создания
- Hashable = можно использовать как ключ/элемент в set
- Обычно они совпадают, но это не обязательно
- Все встроенные неизменяемые типы хэшируемы, обратное неверно
- Для production кода важна оба: неизменяемость для безопасности, хэшируемость для эффективности