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

Могут ли быть объекты класса ключом словаля

1.0 Junior🔥 131 комментариев
#Другое

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

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

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

Могут ли объекты класса быть ключом словаря

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

Основное правило

Объект может быть ключом словаря только если он хешируемый (hashable).

Хешируемый объект — это объект, который:

  1. Имеет метод __hash__(), возвращающий целое число (хеш)
  2. Имеет метод __eq__() для сравнения
  3. Неизменяемый — его хеш не меняется за время жизни

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

# Хешируемые типы — могут быть ключами
d = {}
d[42] = "number"
d["key"] = "string"
d[(1, 2)] = "tuple"
d[frozenset([1, 2])] = "frozenset"
d[True] = "boolean"

print(d)
# {42: 'number', 'key': 'string', (1, 2): 'tuple', frozenset({1, 2}): 'frozenset', True: 'boolean'}

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

# НЕ хешируемые типы — нельзя использовать как ключи
d = {}

# TypeError: unhashable type: 'list'
try:
    d[[1, 2]] = "list"
except TypeError as e:
    print(e)

# TypeError: unhashable type: 'dict'
try:
    d[{"key": "value"}] = "dict"
except TypeError as e:
    print(e)

# TypeError: unhashable type: 'set'
try:
    d[{1, 2}] = "set"
except TypeError as e:
    print(e)

Пользовательские классы по умолчанию хешируемы

По умолчанию, экземпляры пользовательского класса хешируемы.

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

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

# Работает! Объекты класса хешируемы
d = {}
d[p1] = "first point"
d[p2] = "second point"

print(d)
# {<__main__.Point object at 0x...>: 'first point', <__main__.Point object at 0x...>: 'second point'}

print(len(d))  # 2 (два разных объекта, разные хеши)

Почему два разных ключа? По умолчанию используется id() объекта как хеш.

print(hash(p1))  # 140188... (зависит от id)
print(hash(p2))  # 140189... (другой id)
print(p1 == p2)  # False (по умолчанию сравнивает по id)

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

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

p1 = Point(1, 2)
d = {p1: "original"}

# Изменяем объект
p1.x = 5

# Найти объект по ключу станет невозможно!
print(d.get(p1))  # None (проблема!)

Решение 1: Реализовать __hash__ и __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

p1 = Point(1, 2)
p2 = Point(1, 2)  # Другой объект, но равные координаты
p3 = Point(2, 3)

d = {}
d[p1] = "first"
d[p2] = "second"  # Перезапишет p1, так как p1 == p2
d[p3] = "third"

print(d)
# {<Point (1, 2)>: 'second', <Point (2, 3)>: 'third'}

print(len(d))  # 2 (p1 и p2 один ключ)
print(p1 is p2)  # False (разные объекты)
print(p1 == p2)  # True (равные значения)

Решение 2: Использовать @dataclass с frozen=True

from dataclasses import dataclass

@dataclass(frozen=True)  # Неизменяемый класс
class Point:
    x: int
    y: int
    # Автоматически генерируются __hash__ и __eq__

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

d = {p1: "first", p2: "second"}
print(len(d))  # 1 (один ключ, так как p1 == p2)

Решение 3: Использовать @dataclass с unsafe_hash=True

from dataclasses import dataclass

@dataclass(unsafe_hash=True)  # Генерирует __hash__ для изменяемого класса
class Point:
    x: int
    y: int

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

d = {p1: "first"}
print(d[p1])  # "first"
print(d[p2])  # "first" (найден, так как p1 == p2)

# Но внимание: изменение нарушит хеш таблицу!
p1.x = 5
print(d.get(p1))  # Может вернуть неправильный результат

Практический пример: кэш на основе объектов

from dataclasses import dataclass
from functools import lru_cache

@dataclass(frozen=True)
class QueryParams:
    """Параметры запроса как ключ кэша"""
    user_id: int
    limit: int = 10
    offset: int = 0

def cache_by_params(params: QueryParams) -> list:
    """Кэшируем результаты по параметрам"""
    print(f"Запрос БД с параметрами: {params}")
    return [f"item_{i}" for i in range(params.limit)]

# Используем как ключ словаря
cache = {}

params1 = QueryParams(user_id=1, limit=10)
params2 = QueryParams(user_id=1, limit=10)  # Одинаковые параметры

cache[params1] = cache_by_params(params1)
cache[params2] = cache[params1]  # Один ключ!

print(len(cache))  # 1
print(cache[params1])  # Из кэша

Сравнение подходов

ПодходХешируемыйКодИспользование
Встроенный по умолчаниюДаНичегоНе рекомендуется, непредсказуемо
Реализовать hash/eqДаСреднийДля старых классов
@dataclass(frozen=True)ДаМинимумРекомендуется для новых
@dataclass(unsafe_hash=True)ДаМинимумТолько если знаешь что делаешь

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

  1. Если определишь __eq__, определи и __hash__ — или сделай оба None
  2. Неизменяемость — для хешируемого объекта его хеш не должен меняться
  3. Если a == b, то hash(a) == hash(b)
  4. Не используй изменяемые объекты как ключи — приводит к багам
  5. @dataclass(frozen=True) — проверенный способ создать хешируемый класс

Антипаттерн

# ПЛОХО — неизменяемость не гарантирована
class User:
    def __init__(self, id, name):
        self.id = id
        self.name = name
    
    def __hash__(self):
        return hash(self.id)
    
    def __eq__(self, other):
        return self.id == other.id

user = User(1, "Alice")
d = {user: "data"}

user.name = "Bob"  # Изменили объект!
print(d[user])  # Ещё работает, но нарушена инвариант

# ХОРОШО
@dataclass(frozen=True)
class User:
    id: int
    name: str
    # Невозможно изменить после создания