Перейти к содержанию

252 уроков, 17 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 12 из 20 Средний 35 мин 130 XP

Сравнение объектов: __eq__ и __hash__

По умолчанию == для своих классов работает как is: два одинаковых счёта оказываются «не равны». Определяем __eq__ по полям, добавляем __hash__ — и объекты работают в словарях и множествах.

Редакция Питоники

В уроке про __str__ и __repr__ объекты научились печататься — теперь научим их сравниваться. По умолчанию == для собственных классов отвечает так же, как is: «это один и тот же объект в памяти». Два счёта с одинаковым номером оказываются «не равны» — и это не то, что нужно бизнес-логике. Чинится определением __eq__, а для словарей и множеств добавляется __hash__.

== против is: равенство и идентичность

is сравнивает идентичность: лежат ли слева и справа один и тот же объект (совпадают ли их id). == вызывает метод __eq__ — метод равенства. По умолчанию __eq__ работает как is, поэтому два разных объекта с одинаковыми полями не равны:

одинаково — не значит один и тот же
class Account:
    def __init__(self, number):
        self.number = number

first = Account("4070")
second = first
third = Account("4070")

print(first is second)
print(first is third)
print(first == third)
Вывод
True
False
False

Определим равенство по делу: счета равны, если совпадают номера. Сравнивать поля чужого объекта без проверки его класса опасно, поэтому сначала isinstance. Если other вообще не Account, вернём специальный объект NotImplemented: это сигнал «я такой тип не сравниваю», и Python попробует зеркальный вызов other.__eq__(a), а если и там неудача — вернёт False.

равенство по номеру счёта
class Account:
    def __init__(self, number, owner):
        self.number = number
        self.owner = owner

    def __eq__(self, other):
        if not isinstance(other, Account):
            return NotImplemented
        return self.number == other.number

a = Account("4070", "Анна")
b = Account("4070", "Борис")
c = Account("4071", "Анна")

print(a == b)
print(a == c)
print(a != c)
Вывод
True
False
True

__hash__: объект как ключ словаря

Словари и множества раскладывают объекты по ячейкам с помощью функции hash(). Инвариант железный: если a == b, то hash(a) обязан равняться hash(b). Как только вы определили __eq__, Python обнуляет __hash__ — класс становится нехешируемым, и его объекты перестают работать ключами и элементами множеств. Определите __hash__ по тем же полям, что и равенство:

купоны в словаре и множестве
class Coupon:
    def __init__(self, code):
        self.code = code

    def __eq__(self, other):
        if not isinstance(other, Coupon):
            return NotImplemented
        return self.code == other.code

    def __hash__(self):
        return hash(self.code)

promo = {Coupon("SUMMER"): 15, Coupon("WINTER"): 20}
print(promo[Coupon("SUMMER")])

codes = {Coupon("SUMMER"), Coupon("SUMMER"), Coupon("WINTER")}
print(len(codes))
Вывод
15
2
СитуацияЧто происходит
Определили только __eq____hash__ обнуляется: объект нельзя ключом — TypeError: unhashable type
Определили __eq__ и __hash__ по одним полямИнвариант соблюдён: словарь находит значение по новому, но равному ключу
__hash__ по изменяемому полюПосле изменения поля объект теряется в словаре
__ne__ не определёнPython 3 сам выводит != из __eq__ с точностью до наоборот
Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

class Tag:
    def __init__(self, name):
        self.name = name

a = Tag("sale")
b = a

print(a == b, a is b)
class Point:
    def __init__(self, x):
        self.x = x

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

p1 = Point(3)
p2 = Point(3)
print(p1 == p2, p1 == Point(4))
class Word:
    def __init__(self, text):
        self.text = text

    def __eq__(self, other):
        if not isinstance(other, Word):
            return NotImplemented
        return self.text.lower() == other.text.lower()

    def __hash__(self):
        return hash(self.text.lower())

words = {Word("Да"), Word("да"), Word("нет")}
print(len(words))
Проверь себя
0 / 5

1. Чем == отличается от is?

2. Как работает __eq__ по умолчанию?

3. Что сделает Python, если __eq__ вернул NotImplemented?

4. Что случится с __hash__ после определения __eq__?

5. Почему __hash__ считают по неизменяемым полям?

Карточки терминов
Запомнено: 0 / 6
Практика

Создайте класс Book с полями title и author. Определите __eq__ (равенство по паре title и author; для чужих типов — NotImplemented) и __hash__ (hash от того же кортежа). Создайте две одинаковые книги, выведите результат их сравнения и длину множества из обеих книг.

practice.py
Вопросы и ответы по уроку

Чем == отличается от is в Python?

is проверяет, что слева и справа один и тот же объект в памяти (совпадают id), а == вызывает метод __eq__ и проверяет равенство значений. Для малых чисел и строк они могут совпадать из-за кэширования интерпретатора, но полагаться на это нельзя.

Урок 11: __str__ и __repr__

Почему после определения __eq__ объект нельзя положить в словарь?

Python обнуляет __hash__ у классов с кастомным __eq__ без своего __hash__: нехешируемый объект не может быть ключом. Определите __hash__ по тем же полям, что и равенство, — например hash((self.title, self.author)).

Что такое NotImplemented и чем он отличается от False?

NotImplemented — синглтон-сигнал «эту операцию с таким типом я не поддерживаю»: Python попробует зеркальный вызов другой стороны. False — обычный результат «не равно», который останавливает поиск и может маскировать ошибки сравнения с чужими типами.

Нужно ли определять __ne__?

В Python 3 не нужно: оператор != автоматически выводится из __eq__ с точностью до наоборот. Определяйте __ne__ только если неравенство должно считаться иначе, чем «не равно» — крайне редкий случай.

Понравился урок? Сошлитесь на него

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.