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

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

1.8 Middle🔥 131 комментариев
#Python Core#Архитектура и паттерны

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

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

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

Использование экземпляров класса в качестве ключей словаря

По умолчанию объекты Python используют идентификатор (id) как ключ, что не всегда удобно. Для правильной работы нужно реализовать два метода.

1. Что нужно реализовать

Требование: объект можно использовать как ключ, только если он hashable и immutable.

Для этого нужно определить два метода:

class MyClass:
    def __hash__(self):
        """Возвращает целое число (хеш объекта)"""
        return hash((self.attr1, self.attr2))
    
    def __eq__(self, other):
        """Проверяет равенство двух объектов"""
        if not isinstance(other, MyClass):
            return False
        return self.attr1 == other.attr1 and self.attr2 == other.attr2

2. Практический пример

class User:
    def __init__(self, user_id: int, name: str):
        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(id={self.user_id}, name={self.name})"

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

user_data = {}
user_data[user1] = "admin"
user_data[user2] = "moderator"  # Перезапишет user1 (т.к. __eq__ совпадает)
user_data[user3] = "user"

print(user_data)  # {User(id=1, name=Alice): 'moderator', User(id=2, name=Bob): 'user'}
print(user_data[user1])  # 'moderator' (т.к. user1 == user2)

3. Важные правила

Правило 1: если a == b, то hash(a) == hash(b)

# Неправильно:
class BadUser:
    def __init__(self, name):
        self.name = name
    
    def __hash__(self):
        return hash(self.name)
    
    def __eq__(self, other):
        # Проверяем всё, но хеш только по name
        return (self.name == other.name and 
                self.age == other.age)  # BAD!

# Если два пользователя с одинаковыми name, но разными age:
# hash(user1) == hash(user2), но user1 != user2
# Словарь сломается!

Правило 2: значения для хеша должны быть immutable

# Неправильно:
class BadPoint:
    def __init__(self, x, y):
        self.coords = [x, y]  # Mutable list!
    
    def __hash__(self):
        return hash(tuple(self.coords))
    
    def __eq__(self, other):
        return self.coords == other.coords

p1 = BadPoint(1, 2)
points = {p1: "first"}

p1.coords[0] = 5  # Изменили объект
# Теперь hash изменился, но словарь не знает про это
print(p1 in points)  # False! (но объект находится в словаре)

Решение: используй immutable объекты

class GoodPoint:
    def __init__(self, x: int, y: int):
        self._x = x
        self._y = y
    
    def __hash__(self):
        return hash((self._x, self._y))
    
    def __eq__(self, other):
        if not isinstance(other, GoodPoint):
            return False
        return self._x == other._x and self._y == other._y
    
    # Опционально: make truly immutable
    def __setattr__(self, name, value):
        if hasattr(self, '_x'):
            raise AttributeError("GoodPoint is immutable")
        super().__setattr__(name, value)

4. Использование @dataclass

Самый удобный способ с Python 3.7+:

from dataclasses import dataclass
from typing import NamedTuple

# Способ 1: dataclass с frozen=True
@dataclass(frozen=True)  # Immutable и автоматический __hash__
class Point:
    x: int
    y: int

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

print(p1 == p2)  # True
print(hash(p1) == hash(p2))  # True

points = {p1: "first", p2: "second"}  # p2 перезапишет p1
print(points)  # {Point(x=1, y=2): 'second'}

# Способ 2: NamedTuple (уже hashable)
from typing import NamedTuple

class PointNT(NamedTuple):
    x: int
    y: int

p1 = PointNT(1, 2)
p2 = PointNT(1, 2)
points = {p1: "first", p2: "second"}  # Работает как dataclass

5. Обычный dataclass без frozen (осторожно!)

from dataclasses import dataclass

@dataclass
class User:
    user_id: int
    name: str
    # По умолчанию НЕ hashable!

user = User(1, "Alice")
# users = {user: "admin"}  # TypeError: unhashable type: 'User'

# Нужно явно сделать hashable:
@dataclass(frozen=True)
class User:
    user_id: int
    name: str
    # Теперь hashable и immutable

6. Пример с более сложным классом

from dataclasses import dataclass
from typing import Tuple

@dataclass(frozen=True)
class Movie:
    title: str
    year: int
    director: str
    
    def __hash__(self):
        # Можно переопределить для контроля
        # Например, хешировать только title и year
        return hash((self.title, self.year))
    
    def __eq__(self, other):
        if not isinstance(other, Movie):
            return False
        # Два фильма одинаковые, если title и year совпадают
        # (режиссер может быть разным в разных изданиях)
        return self.title == other.title and self.year == other.year

# Использование
movies_ratings = {}

movie1 = Movie("Inception", 2010, "Nolan")
movie2 = Movie("Inception", 2010, "Someone Else")  # Разный режиссер
movie3 = Movie("Interstellar", 2014, "Nolan")

movies_ratings[movie1] = 8.8
movies_ratings[movie2] = 8.9  # Перезапишет movie1 (т.к. __eq__)
movies_ratings[movie3] = 8.7

print(movies_ratings)
print(movie1 in movies_ratings)  # True (т.к. movie1 == movie2)

7. Когда НЕ использовать объекты как ключи

# Проблема: объекты с many-to-one связями
class Author:
    def __init__(self, name):
        self.name = name
    
    def __hash__(self):
        return hash(self.name)
    
    def __eq__(self, other):
        return self.name == other.name

author1 = Author("Tolkien")
author2 = Author("Tolkien")  # Разные объекты, но == True

# Если нужен уникальный идентификатор, используй ID, не name
class GoodAuthor:
    def __init__(self, author_id, name):
        self.author_id = author_id
        self.name = name
    
    def __hash__(self):
        return hash(self.author_id)  # Уникальный ID
    
    def __eq__(self, other):
        return self.author_id == other.author_id

8. Best practices

# ✓ Хорошо: используй frozen dataclass
from dataclasses import dataclass

@dataclass(frozen=True)
class Config:
    host: str
    port: int

config_cache = {}
config = Config("localhost", 8000)
config_cache[config] = {"settings": "..."}

# ✓ Хорошо: используй только уникальные поля
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):
        return isinstance(other, User) and self.user_id == other.user_id

# ✗ Плохо: изменяемые объекты
class MutableUser:
    def __init__(self, user_id):
        self.user_id = user_id
        self.roles = []  # Можно изменять!
    # Не делай hashable изменяемые классы

Итог

Для использования объектов как ключей словаря:

  1. Реализуй __hash__() — возвращающий целое число
  2. Реализуй __eq__() — сравнение объектов
  3. Убедись, что hash(a) == hash(b) если a == b
  4. Используй только immutable поля для хеша
  5. Лучше всего — @dataclass(frozen=True) из коробки