Как правильно реализовать хешируемый класс в Python

Ответ

Чтобы объект класса можно было использовать в качестве ключа словаря (dict) или элемента множества (set), он должен быть хешируемым. Это означает, что у него должен быть постоянный хеш-код на протяжении всей его жизни.

Основные требования:

  1. Неизменяемость (Immutability): Все атрибуты, участвующие в вычислении хеша, должны быть неизменяемыми. Если хеш изменится после добавления объекта в словарь, объект станет недоступным.
  2. Реализация __hash__(self): Метод должен возвращать целочисленное значение (хеш).
  3. Реализация __eq__(self, other): Метод для сравнения объектов. Если a == b, то обязательно должно быть hash(a) == hash(b).

Современный подход (Python 3.7+): dataclasses

Самый простой и надежный способ — использовать декоратор @dataclass(frozen=True). Он автоматически генерирует __eq__ и __hash__ на основе атрибутов класса.

from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: int
    y: int

p1 = Point(1, 2)
p2 = Point(1, 2)
points = {p1: "Начало"}

print(p1 == p2)      # True
print(points[p2])    # Выведет: "Начало"
# p1.x = 3           # Вызовет FrozenInstanceError

Классический подход: ручная реализация

Если вы не можете использовать dataclasses, методы нужно реализовать вручную:

class PointManual:
    def __init__(self, x, y):
        self._x = x
        self._y = y

    def __eq__(self, other):
        if not isinstance(other, PointManual):
            return NotImplemented
        return self.x == other.x and self.y == other.y

    def __hash__(self):
        # Хеш вычисляется на основе кортежа, т.к. кортежи неизменяемы
        return hash((self._x, self._y))

Ответ 18+ 🔞

Смотри, есть такая хуйня: если ты хочешь запихнуть свой самопальный объект в словарь как ключ или в сет, он должен быть хешируемым. А это, блядь, целая наука, на самом деле.

Короче, правила такие, ёпта:

  1. Незыблемость, как скала. Все поля, по которым считается хеш, должны быть неизменными. Представь: положил ты объект в словарь, а потом взял и поменял ему атрибут. Хеш изменился, и всё — объект потерялся в недрах словаря, как хуй в проруби. Достать не получится, пиздец.
  2. Нужен метод __hash__(self). Он должен выдать целое число — это и есть твой хеш.
  3. И метод __eq__(self, other) в придачу. Чтобы сравнивать объекты. Главное правило, которое нельзя нарушать, даже если мать родная попросит: если a == b возвращает True, то hash(a) == hash(b) обязательно должно быть True. Иначе тебе пизда, и интерпретатор тебя сожрёт.

Современный способ, для ленивых и умных: dataclasses

Не еби себе мозг, просто используй @dataclass(frozen=True). Он сам всё сделает: и __eq__, и __hash__ нагенерит на основе твоих полей. А frozen=True — это чтобы объект стал неизменяемым, как ледышка, и никто не смог его случайно испортить.

from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: int
    y: int

p1 = Point(1, 2)
p2 = Point(1, 2)
points = {p1: "Начало"}

print(p1 == p2)      # True
print(points[p2])    # Выведет: "Начало"
# p1.x = 3           # Вызовет FrozenInstanceError — попробуй, сука, измени!

Старый дедовский способ, если ты мазохист

Ну или если ты на древнем коде работаешь. Тогда всё руками, как в старину:

class PointManual:
    def __init__(self, x, y):
        self._x = x
        self._y = y

    def __eq__(self, other):
        if not isinstance(other, PointManual):
            return NotImplemented
        return self.x == other.x and self.y == other.y

    def __hash__(self):
        # Хеш считаем от кортежа из полей — кортежи неизменяемы, и хеш у них стабильный, красота.
        return hash((self._x, self._y))

Вот и вся магия. Главное — не накосячить с неизменяемостью, а то будет тебе волнение ебать, когда ключи начнут пропадать.