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

Как работает __eq__ если использовать свой класс как ключ в словаре?

2.2 Middle🔥 201 комментариев
#Python Core#Soft Skills

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

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

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

Как работает __eq__ если использовать свой класс как ключ в словаре?

Это отличный вопрос, который проверяет глубокое понимание того, как Python работает с хешированием и равенством объектов. Когда вы используете пользовательский класс как ключ в словаре, нужно одновременно реализовать __eq__ и __hash__.

Основные компоненты

1. Роль __hash__ и __eq__

Когда вы добавляете элемент в словарь, Python сначала вычисляет хеш ключа, а затем использует __eq__ для разрешения коллизий:

class User:
    def __init__(self, id, name):
        self.id = id
        self.name = name
    
    def __eq__(self, other):
        if not isinstance(other, User):
            return False
        return self.id == other.id
    
    def __hash__(self):
        return hash(self.id)

# Использование как ключ в словаре
users_dict = {}
user1 = User(1, "Alice")
user2 = User(1, "Bob")  # Разное имя, но одинаковый id

users_dict[user1] = "data1"
users_dict[user2] = "data2"  # Перезапишет первый элемент!

print(len(users_dict))  # 1, потому что user1 == user2
print(users_dict[user1])  # "data2"

Правило согласованности: контракт между __hash__ и __eq__

Это критически важно: если два объекта равны (a == b), то они ДОЛЖНЫ иметь одинаковый хеш (hash(a) == hash(b))

# ❌ НЕПРАВИЛЬНО - нарушает контракт
class BadUser:
    def __init__(self, id):
        self.id = id
    
    def __eq__(self, other):
        return self.id == other.id
    
    def __hash__(self):
        # Возвращаем случайное число - НЕПРАВИЛЬНО!
        import random
        return random.randint(1, 1000000)

u1 = BadUser(1)
u2 = BadUser(1)
print(u1 == u2)  # True - они равны
print(hash(u1) == hash(u2))  # Вероятно False - НАРУШЕНИЕ КОНТРАКТА!

d = {}
d[u1] = "value1"
d[u2] = "value2"  # Может создать два элемента вместо перезаписи

Практический пример: как Python ищет ключ в словаре

class Product:
    def __init__(self, sku, name):
        self.sku = sku
        self.name = name
    
    def __eq__(self, other):
        if not isinstance(other, Product):
            return False
        return self.sku == other.sku
    
    def __hash__(self):
        return hash(self.sku)

# Шаг 1: Вычисление хеша
product1 = Product("SKU001", "Laptop")
h = hash(product1)  # hash("SKU001")

# Шаг 2: Поиск в бакете с этим хешем
inventory = {}
inventory[product1] = 50  # Сохраняет в бакет h

# Шаг 3: Поиск по новому объекту
product2 = Product("SKU001", "Computer")
# Python вычисляет hash(product2) - получает тот же h
# Затем сравнивает: product1 == product2 (вызывает __eq__)
# Они равны, поэтому возвращает имеющееся значение

print(inventory[product2])  # 50

Что происходит, если не переопределить __hash__?

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
    
    # Забыли переопределить __hash__!

p1 = Point(1, 2)
p2 = Point(1, 2)

print(p1 == p2)  # True - они равны

d = {}
d[p1] = "first"
d[p2] = "second"

print(len(d))  # 2 - оба сохранились!
print(d[p1])  # "second"

# ПРОБЛЕМА: hash(p1) != hash(p2)
# Потому что Python использует тождество объекта (id) для хеша по умолчанию
print(hash(p1) == hash(p2))  # False

Автоматическое отключение __hash__

При переопределении __eq__ без __hash__, Python 3 отключает хеширование:

class BadDesign:
    def __init__(self, value):
        self.value = value
    
    def __eq__(self, other):
        return self.value == other.value
    
    # __hash__ НЕ определён

try:
    obj = BadDesign(1)
    d = {obj: "data"}  # TypeError: unhashable type: BadDesign
except TypeError as e:
    print(f"Ошибка: {e}")

Решение: явно установить __hash__

class GoodDesign:
    def __init__(self, value):
        self.value = value
    
    def __eq__(self, other):
        if not isinstance(other, GoodDesign):
            return False
        return self.value == other.value
    
    def __hash__(self):
        return hash(self.value)

obj1 = GoodDesign(1)
obj2 = GoodDesign(1)

d = {obj1: "first"}
d[obj2] = "second"  # Перезаписывает

print(len(d))  # 1
print(d[obj1])  # "second"

Использование кортежей неизменяемых полей

Если вы хешируете по нескольким полям, используйте кортеж:

class Configuration:
    def __init__(self, host, port, timeout):
        self.host = host
        self.port = port
        self.timeout = timeout
    
    def __eq__(self, other):
        if not isinstance(other, Configuration):
            return False
        return (self.host == other.host and 
                self.port == other.port and 
                self.timeout == other.timeout)
    
    def __hash__(self):
        # Кортеж хешируется правильно
        return hash((self.host, self.port, self.timeout))

# Работает как ожидается
config_cache = {}
cfg1 = Configuration("localhost", 8080, 30)
cfg2 = Configuration("localhost", 8080, 30)

config_cache[cfg1] = "config_data"
print(len(config_cache))  # 1 - они считаются одним ключом

Опасность: изменяемые объекты как ключи

Никогда не используйте изменяемые объекты как ключи словаря:

class MutableKey:
    def __init__(self, items):
        self.items = items  # Список - изменяемый!
    
    def __eq__(self, other):
        return self.items == other.items
    
    def __hash__(self):
        return hash(tuple(self.items))

key1 = MutableKey([1, 2, 3])
d = {key1: "value"}

# Изменяем ключ
key1.items.append(4)

# Теперь хеш изменился!
print(hash(key1))  # Другой хеш, чем раньше

# Это поломает словарь - не сможете найти ключ
try:
    print(d[key1])
except KeyError:
    print("Ключ потерялся!")

Правильное использование для enum-подобных значений

from dataclasses import dataclass

@dataclass(frozen=True)  # frozen=True делает неизменяемым
class Status:
    code: int
    name: str

# frozen dataclass автоматически имеет __hash__ и __eq__
statuses = {}
s1 = Status(1, "Active")
s2 = Status(1, "Active")

statuses[s1] = "active_config"
statuses[s2] = "another_config"

print(len(statuses))  # 1 - они идентичны как ключи

Итоговое правило: Если переопределяете __eq__, ВСЕГДА переопределяйте __hash__ так, чтобы объекты, равные по __eq__, имели одинаковый хеш. Лучше всего для этого использовать кортежи неизменяемых полей и dataclass с frozen=True.

Как работает __eq__ если использовать свой класс как ключ в словаре? | PrepBro