Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Могут ли объекты класса быть ключом словаря
Да, объекты класса могут быть ключами словаря, но с важными условиями. Это один из часто неправильно понимаемых аспектов Python.
Основное правило
Объект может быть ключом словаря только если он хешируемый (hashable).
Хешируемый объект — это объект, который:
- Имеет метод
__hash__(), возвращающий целое число (хеш) - Имеет метод
__eq__()для сравнения - Неизменяемый — его хеш не меняется за время жизни
Встроенные хешируемые типы
# Хешируемые типы — могут быть ключами
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) | Да | Минимум | Только если знаешь что делаешь |
Важные правила
- Если определишь
__eq__, определи и__hash__— или сделай оба None - Неизменяемость — для хешируемого объекта его хеш не должен меняться
- Если a == b, то hash(a) == hash(b)
- Не используй изменяемые объекты как ключи — приводит к багам
- @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
# Невозможно изменить после создания