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

Зачем нужен метод __hash__?

2.3 Middle🔥 191 комментариев
#Python Core

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

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

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

Метод hash в Python

Метод __hash__ нужен для получения хеша объекта — целого числа, которое используется для быстрого поиска и хранения объектов в хеш-таблицах (dict, set).

Для чего нужен хеш?

Хеш-функция преобразует объект в целое число, которое используется как индекс в хеш-таблице. Это позволяет искать объекты за O(1) вместо O(n).

user1 = {"name": "Alice", "age": 30}
user2 = {"name": "Bob", "age": 25}

# Хеш используется для быстрого поиска
print(hash(user1))  # Ошибка! dict не hashable

# Но для tuple работает
key = ("user", 123)
print(hash(key))  # 8015626505930503568

# Это число используется как индекс:
cache = {}
cache[key] = "cached value"
# Внутри cache[key] использует hash(key) для быстрого доступа

Использование в dict и set

Словари используют хеш для быстрого поиска ключей:

user_cache = {
    ("alice", 30): "cached_data_1",
    ("bob", 25): "cached_data_2"
}

# Поиск за O(1)
print(user_cache[("alice", 30)])  # "cached_data_1"

# Внутри Python:
# 1. Вычислить hash(("alice", 30)) → 12345 (пример)
# 2. Перейти по индексу 12345 % table_size
# 3. Вернуть значение

Множества используют хеш для проверки уникальности:

unique_users = {
    ("alice", 30),
    ("bob", 25),
    ("alice", 30)  # Дубликат
}

print(unique_users)  # {("bob", 25), ("alice", 30)}
print(len(unique_users))  # 2

# Внутри set используется хеш для проверки уникальности
# Если hash("alice", 30) уже есть → дубликат, не добавляем

Создание собственного hash

Пример 1: Простой класс

class User:
    def __init__(self, username, user_id):
        self.username = username
        self.user_id = user_id
    
    def __hash__(self):
        # Хеш должен быть основан на неизменяемых атрибутах
        return hash((self.username, self.user_id))
    
    def __eq__(self, other):
        # Если объекты equal, их хеши должны быть одинаковыми
        if not isinstance(other, User):
            return False
        return self.username == other.username and self.user_id == other.user_id
    
    def __repr__(self):
        return f"User({self.username}, {self.user_id})"

# Теперь объекты User можно использовать в dict и set
user1 = User("alice", 1)
user2 = User("alice", 1)  # Такой же
user3 = User("bob", 2)    # Другой

user_set = {user1, user2, user3}  # set нужен __hash__
print(len(user_set))  # 2 (user1 и user2 — дубликаты)
print(user1 == user2)  # True
print(hash(user1) == hash(user2))  # True

user_dict = {user1: "data_alice", user3: "data_bob"}
print(user_dict[user2])  # "data_alice" (user2 == user1)

Пример 2: Сложный класс с несколькими атрибутами

class Point:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
    
    def __hash__(self):
        # Хеш всех координат
        return hash((self.x, self.y, self.z))
    
    def __eq__(self, other):
        if not isinstance(other, Point):
            return False
        return self.x == other.x and self.y == other.y and self.z == other.z
    
    def __repr__(self):
        return f"Point({self.x}, {self.y}, {self.z})"

points = {
    Point(0, 0, 0),
    Point(1, 2, 3),
    Point(0, 0, 0),  # Дубликат
}
print(len(points))  # 2

# Можно использовать как ключи dict
point_data = {
    Point(0, 0, 0): "origin",
    Point(1, 2, 3): "other"
}
print(point_data[Point(0, 0, 0)])  # "origin"

Важный контракт: hash и eq

Правило: Если два объекта равны (a == b), их хеши должны быть одинаковыми (hash(a) == hash(b)).

Обратное не обязательно (разные объекты могут иметь одинаковый хеш — collision).

# ✅ Правильно
class Good:
    def __init__(self, value):
        self.value = value
    
    def __hash__(self):
        return hash(self.value)
    
    def __eq__(self, other):
        return isinstance(other, Good) and self.value == other.value

# Проверка
a = Good(42)
b = Good(42)
print(a == b)  # True
print(hash(a) == hash(b))  # True ✅ (как и положено)

# ❌ НЕПРАВИЛЬНО!
class Bad:
    def __init__(self, value):
        self.value = value
    
    def __hash__(self):
        return 42  # Всегда один и тот же хеш
    
    def __eq__(self, other):
        return isinstance(other, Bad) and self.value == other.value

# Проверка
x = Bad("foo")
y = Bad("foo")
z = Bad("bar")

print(x == y)  # True
print(hash(x) == hash(y))  # True ✅

# Но в set происходит проблема:
my_set = {x, y, z}
print(len(my_set))  # 2 (ожидаем)

bad_set = {x, z}
print(z in bad_set)  # True
print(Bad("bar") in bad_set)  # False! Ошибка!
# Хеши одинаковые, поэтому Python проверит __eq__,
# но Bad("bar") != z по объектной идентичности? Нет, по значению!
# На самом деле должно быть True

Mutable vs Immutable

Immutable объекты (неизменяемые) можно хешировать:

# ✅ int, str, tuple, frozenset — hashable
hashable = {
    42,
    "hello",
    (1, 2, 3),
    frozenset([1, 2])
}
print(len(hashable))  # 4

dict_key = {42: "forty-two", "hello": "greeting"}
print(dict_key[42])  # "forty-two"

# ❌ list, dict, set — НЕ hashable (mutable)
try:
    bad_set = {[1, 2, 3]}  # Ошибка!
except TypeError as e:
    print(f"Ошибка: {e}")  # TypeError: unhashable type: list

try:
    bad_dict = {{"a": 1}: "value"}  # Ошибка!
except TypeError as e:
    print(f"Ошибка: {e}")  # TypeError: unhashable type: dict

Почему mutable объекты не hashable?

Если объект изменяется, его хеш меняется. Это ломает инварианты dict/set:

user = User("alice", 1)
user_set = {user}

# Если мы позволим изменить user
user.username = "bob"  # Хеш изменится!

# Теперь объект в неправильном месте в хеш-таблице
print(user in user_set)  # Может быть False!
# Опасно!

Поэтому Python по умолчанию не хеширует mutable объекты.

Когда писать hash?

Пишите hash, если:

  • Хотите использовать объект как ключ в dict
  • Хотите добавить объект в set
  • Нужна быстрая проверка уникальности
  • Объект immutable или поведение immutable

Не пишите hash, если:

  • Объект mutable (может изменяться)
  • Не планируете использовать в dict/set
  • По умолчанию класс уже hashable (tuple, frozenset)

Пример: Кэширование с использованием hash

class DataCache:
    def __init__(self):
        self.cache = {}  # dict использует hash как ключ
    
    def get(self, key):
        return self.cache.get(key)
    
    def set(self, key, value):
        self.cache[key] = value

class Query:
    def __init__(self, sql, params=None):
        self.sql = sql
        self.params = params or tuple()
    
    def __hash__(self):
        return hash((self.sql, self.params))
    
    def __eq__(self, other):
        return (isinstance(other, Query) and 
                self.sql == other.sql and 
                self.params == other.params)

# Использование
cache = DataCache()

query1 = Query("SELECT * FROM users WHERE id = ?", (1,))
cache.set(query1, [{"id": 1, "name": "Alice"}])

# Поиск по эквивалентному запросу
query2 = Query("SELECT * FROM users WHERE id = ?", (1,))
result = cache.get(query2)  # Найдет! Так как hash и eq совпадают
print(result)  # [{"id": 1, "name": "Alice"}]

Заключение

__hash__ — это критический метод для работы с dict и set. Правильная реализация:

  • Основана на immutable атрибутах
  • Совместима с __eq__ (если a == b, то hash(a) == hash(b))
  • Дает разумное распределение хешей
  • Используется для O(1) поиска и проверки уникальности

Это фундаментальный паттерн в Python, без которого не обойтись при работе с коллекциями и кэшированием.