Сравнение объектов: __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))
1. Чем == отличается от is?
2. Как работает __eq__ по умолчанию?
3. Что сделает Python, если __eq__ вернул NotImplemented?
4. Что случится с __hash__ после определения __eq__?
5. Почему __hash__ считают по неизменяемым полям?
Создайте класс Book с полями title и author. Определите __eq__ (равенство по паре title и author; для чужих типов — NotImplemented) и __hash__ (hash от того же кортежа). Создайте две одинаковые книги, выведите результат их сравнения и длину множества из обеих книг.
Чем == отличается от 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-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
ООП · Урок 11
Магические методы: __str__ и __repr__
print(obj) без подготовки показывает тип и адрес в памяти. Магические методы __str__ и __repr__ позволяют самим решить, как объект превращается в строку, — человек увидит «21°C», а отладчик — Temperature(21).
ООП · Урок 13
Перегрузка операторов: __add__ и __getitem__
Плюс склеивает строки, объединяет списки и складывает числа — за каждым знаком стоит магический метод. Научим вектор складываться с вектором, плейлист — отвечать на индексы и срезы, а len() — считать элементы.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
json · Урок 7
Вложенные структуры: списки в словарях и наоборот
Настоящие данные не плоские: список товаров внутри заказа, массив объектов в ответе API. Учимся читать вложенный JSON сверху вниз и не теряться в глубине.
json массив объектов pythonсписок внутри словаря python
NumPy · Урок 4
Операции с массивами: векторизация и почему NumPy быстрее списков в 100 раз
Умножаем миллион цен одной строкой и замеряем время через time.perf_counter: почему векторизация NumPy обгоняет цикл for в десятки раз. Ufunc, np.dot, булевы сравнения и правило «никаких for там, где есть операция».
сравнение массивов numpy
ООП · Урок 1
Классы в Python: первый объект
Класс — чертёж, объект — экземпляр по этому чертежу. Создаём первый класс, вызываем методы и убеждаемся: в Python объектом является всё, включая функции.
объект pythontype python функция