Можно ли создать хэшируемый пользовательский класс в Python?

Ответ

Да, для этого необходимо корректно реализовать два специальных метода: __hash__ и __eq__. Это позволит использовать экземпляры класса как ключи в словарях или элементы множеств.

Ключевые правила:

  1. Метод __eq__ определяет логику сравнения объектов на равенство.
  2. Метод __hash__ должен возвращать целочисленное значение, которое неизменно на протяжении всего времени жизни объекта.
  3. Если a == b (т.е. __eq__ возвращает True), то обязательно должно выполняться hash(a) == hash(b).

Пример реализации неизменяемого (immutable) класса:

class ImmutablePoint:
    """Пример хэшируемого класса, представляющего точку."""
    __slots__ = ('_x', '_y')  # Оптимизация памяти и запрет на добавление атрибутов

    def __init__(self, x, y):
        # Атрибуты делаем приватными и не изменяемыми после создания
        self._x = x
        self._y = y

    @property
    def x(self):
        return self._x

    @property
    def y(self):
        return self._y

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

    def __eq__(self, other):
        if not isinstance(other, ImmutablePoint):
            return NotImplemented
        return (self._x, self._y) == (other._x, other._y)

# Использование
p1 = ImmutablePoint(1, 2)
p2 = ImmutablePoint(1, 2)
print(p1 == p2)  # True
print(hash(p1) == hash(p2))  # True

points_set = {p1, p2}
print(points_set)  # {<__main__.ImmutablePoint object at ...>} - только один элемент
print(p1 in points_set)  # True

Важное предупреждение: Если класс является изменяемым (mutable), и его состояние влияет на __hash__, то изменение объекта после помещения в словарь или множество приведет к его "потере" в этой коллекции, так как хэш изменится. Поэтому хэшируемые классы лучше делать неизменяемыми.

Ответ 18+ 🔞

А, ну это же классика, ёпта! Смотри, если ты хочешь, чтобы твои объекты вели себя как нормальные граждане в словарях и множествах, им нужно выучить два магических заклинания: __hash__ и __eq__. Без этого они — просто манда с ушами, которую никуда не воткнешь.

Главное, что надо запомнить, чтобы не накрыться медным тазом:

  1. __eq__ — это когда ты сравниваешь, один и тот же это объект или нет. Типа, «ты мне ровня или хуй с горы?».
  2. __hash__ — должен выдать одно и то же число на протяжении всей жизни объекта. Как отпечаток пальца, только для питона.
  3. САМОЕ ВАЖНОЕ ПРАВИЛО, ЕБААААТЬ: Если a == b (то есть __eq__ сказал «да, это одно и то же»), то и hash(a) == hash(b) ОБЯЗАТЕЛЬНО должен быть правдой. Иначе всё полетит в пиздопроебибну, и доверия к твоему коду будет ебать ноль.

Вот смотри, как это делается на примере точки, которую нельзя сломать:

class ImmutablePoint:
    """Точка, которую не переделаешь под себя — как характер у тёщи."""
    __slots__ = ('_x', '_y')  # Чтоб лишних атрибутов не налепили, хитрая жопа

    def __init__(self, x, y):
        # Делаем атрибуты приватными и замораживаем нахуй
        self._x = x
        self._y = y

    @property
    def x(self):
        return self._x

    @property
    def y(self):
        return self._y

    def __hash__(self):
        # Хэш делаем из кортежа этих самых полей. Просто и гениально.
        return hash((self._x, self._y))

    def __eq__(self, other):
        # Если нам подсунули хуй в пальто, а не точку — говорим «я не могу это сравнить»
        if not isinstance(other, ImmutablePoint):
            return NotImplemented
        # Сравниваем как кортежи — и волнение ебать куда-то делось
        return (self._x, self._y) == (other._x, other._y)

# Проверяем, работает ли эта магия
p1 = ImmutablePoint(1, 2)
p2 = ImmutablePoint(1, 2)
print(p1 == p2)  # True — они одинаковые, ёпта!
print(hash(p1) == hash(p2))  # True — и хэши сошлись, красота!

points_set = {p1, p2}  # Пытаемся засунуть в множество
print(points_set)  # {<__main__.ImmutablePoint object at ...>} — ага, дубля сожрало!
print(p1 in points_set)  # True — и он там есть, всё пучком

И последнее, но овердохуища важное: Если твой класс можно менять после создания (мутабельный), и от этого меняется его хэш — ты играешь с огнём. Представь: положил объект в словарь, потом его изменил, хэш поменялся, и всё — объект как в черную дыру провалился, найти его уже не получится. Поэтому либо делай класс неизменяемым, как в примере, либо будь готов к неожиданностям. Сам от себя охуеешь, когда это всплывёт.