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

Что должно быть у класса чтобы его объекты стали ключом словаря?

2.0 Middle🔥 131 комментариев
#Python Core

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

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

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

Что нужно для использования объекта как ключа словаря

Чтобы объект класса стал ключом словаря (dict), он должен быть хешируемым. Это требует реализации двух методов: __hash__() и __eq__().

Необходимые условия

1. Метод __hash__()

Возвращает целое число, которое однозначно идентифицирует объект:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __hash__(self):
        return hash((self.x, self.y))

p = Point(1, 2)
print(hash(p))  # -9223372036854775807 (числовое значение)

Хеш используется для быстрого поиска ключа в словаре (O(1) вместо O(n)).

2. Метод __eq__()

Сравнивает два объекта на равенство:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __hash__(self):
        return hash((self.x, self.y))
    
    def __eq__(self, other):
        if not isinstance(other, Point):
            return False
        return self.x == other.x and self.y == other.y

# Теперь можно использовать как ключ
coordinates = {}
point1 = Point(1, 2)
point2 = Point(1, 2)  # Разные объекты

coordinates[point1] = "Location A"
print(coordinates[point2])  # "Location A" — они равны по значению

print(point1 == point2)  # True
print(hash(point1) == hash(point2))  # True

Правило консистентности

КРИТИЧЕСКОЕ ПРАВИЛО: Если два объекта равны по __eq__(), их хеши ДОЛЖНЫ быть одинаковы.

# ПРАВИЛЬНО
if a == b:
    assert hash(a) == hash(b)

# НЕПРАВИЛЬНО — ведёт к багам
class BadPoint:
    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
    
    def __hash__(self):
        return id(self)  # ОПАСНО! Разные объекты могут быть равны

p1 = BadPoint(1, 1)
p2 = BadPoint(1, 1)

d = {p1: "value1"}
print(p1 == p2)  # True — они равны
print(hash(p1) == hash(p2))  # False — хеши разные! БАГ!
print(d[p2])  # KeyError! Не найдено, хотя должно быть

Полный рабочий пример

class User:
    """Класс для использования объектов как ключей словаря"""
    
    def __init__(self, user_id, name):
        self.user_id = user_id
        self.name = name
    
    def __hash__(self):
        # Используем только уникальный идентификатор
        return hash(self.user_id)
    
    def __eq__(self, other):
        if not isinstance(other, User):
            return False
        return self.user_id == other.user_id
    
    def __repr__(self):
        return f"User({self.user_id}, {self.name})"

# Использование
user1 = User(1, "Alice")
user2 = User(1, "Alice")  # Другой объект, но с тем же ID
user3 = User(2, "Bob")

# Создать словарь с User в качестве ключей
user_info = {}
user_info[user1] = {"email": "alice@example.com", "role": "admin"}
user_info[user3] = {"email": "bob@example.com", "role": "user"}

# Поиск по другому объекту с тем же ID
print(user_info[user2])  # {"email": "alice@example.com", "role": "admin"}

# Проверка принадлежности
print(user1 in user_info)  # True
print(user2 in user_info)  # True — они считаются одинаковыми
print(user3 in user_info)  # True

print(user1 == user2)  # True
print(user1 is user2)  # False — разные объекты, но одинаковые по значению

Что НЕ нужно делать

1. Не переопределяй только __eq__() без __hash__()

class BadClass:
    def __eq__(self, other):
        return True  # Все равны
    
    # НЕ переопределили __hash__()
    # Python автоматически сделает объект нехешируемым

obj = BadClass()
d = {obj: "value"}  # TypeError: unhashable type: 'BadClass'

2. Не используй изменяемые атрибуты в __hash__()

class BadUser:
    def __init__(self, user_id, permissions: list):
        self.user_id = user_id
        self.permissions = permissions  # ИЗМЕНЯЕМЫЙ
    
    def __hash__(self):
        return hash((self.user_id, tuple(self.permissions)))  # Опасно
    
    def __eq__(self, other):
        return self.user_id == other.user_id

user = BadUser(1, ["read", "write"])
d = {user: "info"}

user.permissions.append("delete")  # Изменили объект!
# Теперь хеш изменился, но объект всё ещё в словаре
# Это нарушает инвариант словаря — БАГ!

3. Не забывай про __repr__()

Добавь это для отладки:

class GoodClass:
    def __init__(self, value):
        self.value = value
    
    def __hash__(self):
        return hash(self.value)
    
    def __eq__(self, other):
        return isinstance(other, GoodClass) and self.value == other.value
    
    def __repr__(self):
        return f"GoodClass({self.value})"

obj = GoodClass(42)
print(obj)  # GoodClass(42) — намного лучше чем <GoodClass object at 0x...>

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

В Python уже есть хешируемые типы:

# Все эти типы можно использовать как ключи
data = {
    42: "int",
    3.14: "float",
    "key": "str",
    (1, 2, 3): "tuple",
    frozenset([1, 2]): "frozenset",
    True: "bool",
    None: "None"
}

# А эти — НЕЛЬЗЯ (изменяемы)
try:
    data[[1, 2, 3]] = "list"  # TypeError
    data[{"a": 1}] = "dict"   # TypeError
    data[{1, 2}] = "set"      # TypeError
except TypeError:
    print("Нельзя использовать изменяемые типы как ключи")

Практический пример: Кэш функции

from functools import lru_cache

class Config:
    def __init__(self, timeout, retry_count):
        self.timeout = timeout
        self.retry_count = retry_count
    
    def __hash__(self):
        return hash((self.timeout, self.retry_count))
    
    def __eq__(self, other):
        return (self.timeout == other.timeout and 
                self.retry_count == other.retry_count)

# Использовать Config как ключ в словаре кэша
def cache_expensive_operation(config: Config):
    cache = {}
    
    def compute():
        if config in cache:
            return cache[config]
        
        result = expensive_operation(config)
        cache[config] = result
        return result
    
    return compute

Итог

Чтобы использовать объект класса как ключ словаря:

  1. Реализовать __hash__() — возвращает целое число
  2. Реализовать __eq__() — сравнивает объекты
  3. Сохранить консистентность — равные объекты должны иметь одинаковый хеш
  4. Использовать только неизменяемые атрибуты в hash()
  5. Добавить __repr__() для удобства отладки

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