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

Как сделать класс хешируемым?

1.0 Junior🔥 91 комментариев
#Python Core

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

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

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

Как сделать класс хешируемым в Python

Хешируемые объекты могут использоваться как ключи словарей и элементы set'ов. Для этого нужно правильно реализовать методы __hash__() и __eq__().

1. Основное правило хешируемости

# Для хешируемого объекта верно: если a == b, то hash(a) == hash(b)
# Обратное неверно: разные объекты могут иметь одинаковый hash

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __eq__(self, other):
        return isinstance(other, Point) and self.x == other.x and self.y == other.y
    
    def __hash__(self):
        return hash((self.x, self.y))
    
    def __repr__(self):
        return f'Point({self.x}, {self.y})'

# Теперь объект хешируем
p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)

print(p1 == p2)          # True
print(hash(p1) == hash(p2))  # True
print(p1 is p2)          # False (разные объекты)

# Можем использовать как ключи
point_dict = {p1: 'origin'}
print(point_dict[p2])  # 'origin' (p1 == p2)

# Можем добавлять в set
point_set = {p1, p2, p3}
print(len(point_set))  # 2 (p1 и p2 считаются одним)
print(point_set)  # {Point(1, 2), Point(3, 4)}

2. Требования к hash()

# Обязательные правила:
# 1. Если a == b, то hash(a) == hash(b)
# 2. Хеш должен быть неизменяемым (пока объект не меняется)
# 3. Используй только неизменяемые поля для хеша

class User:
    def __init__(self, id, name, email):
        self.id = id  # неизменяемо
        self.name = name  # изменяемо
        self.email = email  # изменяемо
    
    def __eq__(self, other):
        return isinstance(other, User) and self.id == other.id
    
    def __hash__(self):
        # Используем ТОЛЬКО неизменяемые поля (id)
        return hash(self.id)
    
    def __repr__(self):
        return f'User({self.id}, {self.name})'

user1 = User(1, 'Alice', 'alice@example.com')
user2 = User(1, 'Alice Updated', 'newemail@example.com')

print(user1 == user2)  # True (сравниваем по id)
print(hash(user1) == hash(user2))  # True

# Даже если меняем поля, хеш остаётся тем же
user1.name = 'Alice Changed'
print(hash(user1))  # Тот же хеш

3. Типичные ошибки

Ошибка 1: Использование изменяемых полей

class BadPoint:
    def __init__(self, coords):
        self.coords = coords  # list - изменяемо!
    
    def __eq__(self, other):
        return isinstance(other, BadPoint) and self.coords == other.coords
    
    def __hash__(self):
        return hash(self.coords)  # ОШИБКА: list not hashable!

# Это не сработает
p = BadPoint([1, 2])
print(hash(p))  # TypeError: unhashable type: 'list'

Решение:

class GoodPoint:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __hash__(self):
        return hash((self.x, self.y))  # tuple - неизменяемо

Ошибка 2: Несогласованность eq и hash

class BadUser:
    def __init__(self, id, name):
        self.id = id
        self.name = name
    
    def __eq__(self, other):
        # Сравниваем по обоим полям
        return isinstance(other, BadUser) and self.id == other.id and self.name == other.name
    
    def __hash__(self):
        # А хешируем только по id - ОШИБКА!
        return hash(self.id)

u1 = BadUser(1, 'Alice')
u2 = BadUser(1, 'Alice')
u3 = BadUser(1, 'Bob')

print(u1 == u2)  # True
print(hash(u1) == hash(u2))  # True ✓

print(u1 == u3)  # False
print(hash(u1) == hash(u3))  # True ✗ ОШИБКА!

# В dict/set это создаёт проблемы
my_set = {u1, u3}
print(len(my_set))  # 1 (вместо 2!) - неопределённое поведение

4. Правильная реализация для разных типов данных

Простой класс с одним идентификатором

class Product:
    def __init__(self, product_id, name, price):
        self.product_id = product_id  # уникальный
        self.name = name
        self.price = price
    
    def __eq__(self, other):
        return isinstance(other, Product) and self.product_id == other.product_id
    
    def __hash__(self):
        return hash(self.product_id)
    
    def __repr__(self):
        return f'Product({self.product_id})'

p1 = Product(1, 'Laptop', 1000)
p2 = Product(1, 'Laptop Pro', 1500)
p3 = Product(2, 'Phone', 500)

products_set = {p1, p2, p3}
print(len(products_set))  # 2 (p1 и p2 одинаковые)
products_dict = {p1: 'in-stock', p3: 'out-of-stock'}
print(products_dict[p2])  # 'in-stock' (p2 == p1)

Класс с несколькими ключевыми полями

class OrderItem:
    def __init__(self, order_id, item_id, quantity):
        self.order_id = order_id
        self.item_id = item_id
        self.quantity = quantity
    
    def __eq__(self, other):
        return (isinstance(other, OrderItem) and 
                self.order_id == other.order_id and 
                self.item_id == other.item_id)
    
    def __hash__(self):
        return hash((self.order_id, self.item_id))
    
    def __repr__(self):
        return f'OrderItem({self.order_id}, {self.item_id})'

item1 = OrderItem('ORD-001', 'ITEM-A', 5)
item2 = OrderItem('ORD-001', 'ITEM-A', 10)  # Другое количество, но он же
item3 = OrderItem('ORD-002', 'ITEM-A', 5)

print(item1 == item2)  # True (игнорируем quantity)
print(hash(item1) == hash(item2))  # True

items_set = {item1, item2, item3}
print(len(items_set))  # 2

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

Для dataclass хеширование настраивается автоматически:

from dataclasses import dataclass

@dataclass(frozen=True)  # frozen=True делает объект неизменяемым и хешируемым
class Location:
    latitude: float
    longitude: float

loc1 = Location(55.7558, 37.6173)  # Москва
loc2 = Location(55.7558, 37.6173)  # Москва
loc3 = Location(59.9311, 30.3609)  # СПб

locations = {loc1, loc2, loc3}
print(len(locations))  # 2
print(hash(loc1) == hash(loc2))  # True

Без frozen=True:

@dataclass
class MutablePoint:
    x: float
    y: float

p = MutablePoint(1.0, 2.0)
print(hash(p))  # TypeError: unhashable type 'MutablePoint'

6. Пользовательский хеш с колизиями

Иногда хешам позволяют иметь коллизии (разные объекты с одинаковым хешем):

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

str_set = {CaseInsensitiveStr('Hello'), CaseInsensitiveStr('HELLO'), CaseInsensitiveStr('hello')}
print(len(str_set))  # 1 (все три - одни и те же)
print(str_set)

7. Невозможные хеши

Некоторые объекты не могут быть хешируемы:

# Нельзя хешировать классы с изменяемыми полями
class MutableUser:
    def __init__(self, id, permissions):
        self.id = id
        self.permissions = permissions  # list - изменяемо

user = MutableUser(1, ['read', 'write'])
print(hash(user))  # TypeError: unhashable type

# Решение 1: использовать tuple вместо list
class ImmutableUser:
    def __init__(self, id, permissions):
        self.id = id
        self.permissions = tuple(permissions)  # неизменяемо
    
    def __hash__(self):
        return hash((self.id, self.permissions))

# Решение 2: использовать __slots__ и frozen dataclass
from dataclasses import dataclass

@dataclass(frozen=True)
class FrozenUser:
    id: int
    permissions: tuple

8. Пример с собственным контейнером

class UniqueUserCollection:
    """Коллекция уникальных пользователей"""
    
    def __init__(self):
        self._users = set()  # используем set для уникальности
    
    def add(self, user):
        if not isinstance(user, User):
            raise TypeError('Expected User instance')
        self._users.add(user)  # требует, чтобы User был хешируемым
    
    def contains(self, user):
        return user in self._users  # O(1) благодаря hash
    
    def __len__(self):
        return len(self._users)
    
    def __iter__(self):
        return iter(self._users)

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

collection = UniqueUserCollection()
collection.add(User(1, 'Alice'))
collection.add(User(1, 'Alice Changed'))  # Не добавится (дубликат по id)
print(len(collection))  # 1

Резюме

Для хешируемого класса нужно:

  1. Реализовать __hash__() — вернуть целое число
  2. Реализовать __eq__() — сравнение объектов
  3. Согласованность — если a == b то hash(a) == hash(b)
  4. Использовать неизменяемые поля — для вычисления хеша
  5. Использовать tuple — если нужно хешировать несколько полей
  6. Рассмотреть frozen dataclass — для простых случаев

Основная формула:

class MyClass:
    def __init__(self, id, name):
        self.id = id  # неизменяемо
        self.name = name
    
    def __eq__(self, other):
        return isinstance(other, MyClass) and self.id == other.id
    
    def __hash__(self):
        return hash(self.id)