В уроке о ссылках == у двух товаров с одинаковыми названием и ценой давал False: по умолчанию свой класс
сравнивается по тождественности, как is. Но для кассы «латте за 220» и ещё один «латте за 220» — одно и то
же. Научим объекты сравниваться по смыслу.
__eq__
Оператор a == b вызывает метод a.__eq__(b). Объявите его — и равенство будет означать то, что нужно
вашему классу. is при этом не меняется: он по-прежнему отвечает, один ли это объект.
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __repr__(self):
return f"Product({self.name!r}, {self.price})"
def __eq__(self, other):
return self.name == other.name and self.price == other.price
a = Product("Латте", 220)
b = Product("Латте", 220)
print(a == b, a is b)
print(a != Product("Латте", 250))True False True
!= Python считает сам — как «не ==», отдельный метод не нужен.
Чужой тип: NotImplemented
Сравнивать можно с чем угодно: latte == "Латте", latte == 220. Но у строки нет атрибутов name и
price, и такой __eq__ падает:
a == "Латте"AttributeError: 'str' object has no attribute 'name'
Поэтому __eq__ сначала проверяет тип другого объекта. Если тип чужой, метод возвращает специальное значение
NotImplemented — «я не умею сравнивать с таким». Тогда Python спросит второй объект: "Латте".__eq__(latte),
и если и он не умеет — сравнит объекты по тождественности, как is: для двух разных объектов это False.
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __repr__(self):
return f"Product({self.name!r}, {self.price})"
def __eq__(self, other):
if not isinstance(other, Product):
return NotImplemented # «не умею» — пусть ответит другой объект
return self.name == other.name and self.price == other.price
latte = Product("Латте", 220)
print(latte == Product("Латте", 220))
print(latte == "Латте", latte == 220)True False False
class Ticket:
"""Строка кассового чека: знает, какому товару соответствует."""
def __init__(self, name):
self.name = name
def __eq__(self, other):
if isinstance(other, (Product, StrictProduct)):
return self.name == other.name
return NotImplemented
class StrictProduct:
def __init__(self, name, price):
self.name = name
self.price = price
def __eq__(self, other):
if not isinstance(other, StrictProduct):
return False # ошибка: не даёт ответить Ticket
return self.name == other.name and self.price == other.price
ticket = Ticket("Латте")
print(Product("Латте", 220) == ticket) # Python спросил Ticket — True
print(StrictProduct("Латте", 220) == ticket) # ответ False, Ticket не спросилиTrue False
Не путайте NotImplemented (значение, которое возвращает магический метод) и NotImplementedError
(исключение) — это разные вещи.
Допишите класс Drink: два напитка равны, если у них совпадают цена и название — без учёта регистра:
кассиры пишут то «Латте», то «латте». Сравнение с объектом другого типа возвращает NotImplemented.
other, как в демонстрации; потом сравните цены и названия.lower(): self.name.lower() == other.name.lower().Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
class Drink:
def __init__(self, name, price):
self.name = name
self.price = price
def __eq__(self, other):
...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
class Drink:
def __init__(self, name, price):
self.name = name
self.price = price
def __eq__(self, other):
if not isinstance(other, Drink):
return NotImplemented
return self.name.lower() == other.name.lower() and self.price == other.price__hash__: объекты в set и ключи dict
set и dict находят элементы не перебором, а по хешу (hash) — числу, которое вычисляет hash(x). Правило:
равные объекты обязаны иметь равные хеши, иначе set положит два «равных» товара в разные места.
Поэтому, как только в классе появляется __eq__, Python отключает хеш по умолчанию — экземпляр перестаёт
быть хешируемым (hashable):
print(Product.__hash__) # __eq__ есть — хеш отключён
{latte}None
TypeError: cannot use 'Product' as a set element (unhashable type: 'Product')
Чтобы вернуть хеш, объявляют __hash__ и вычисляют его только из атрибутов, которые сравнивает __eq__ — из
всех или из части, — но не из других: иначе у равных объектов хеши разойдутся. Проще всего — хеш кортежа из
всех сравниваемых атрибутов: hash((self.name, self.price)).
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __repr__(self):
return f"Product({self.name!r}, {self.price})"
def __eq__(self, other):
if not isinstance(other, Product):
return NotImplemented
return self.name == other.name and self.price == other.price
def __hash__(self):
return hash((self.name, self.price))
shelf = {Product("Латте", 220), Product("Латте", 220), Product("Чай", 120)}
print(len(shelf))
print(Product("Латте", 220) in shelf)
stock = {Product("Чай", 120): 30}
print(stock[Product("Чай", 120)])2 True 30
Сами числа хешей не важны и меняются от запуска к запуску — важно только, что у равных объектов они равны.
Допишите класс Customer: покупатели равны, если у них один и тот же телефон phone (имя может быть записано
по-разному: «Анна» и «Аня»). Сравнение с другим типом — NotImplemented. Добавьте __hash__, чтобы
покупателей можно было класть в set и делать ключами dict.
__eq__.__hash__ — return hash(self.phone).Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
class Customer:
def __init__(self, name, phone):
self.name = name
self.phone = phone
def __eq__(self, other):
...
def __hash__(self):
...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
class Customer:
def __init__(self, name, phone):
self.name = name
self.phone = phone
def __eq__(self, other):
if not isinstance(other, Customer):
return NotImplemented
return self.phone == other.phone
def __hash__(self):
return hash(self.phone)Почему объект пропал из set
Хеш вычисляется в момент, когда объект кладут в set. Если потом изменить атрибут, от которого зависит хеш,
объект окажется «не на своём месте» — set его больше не найдёт:
latte = Product("Латте", 220)
basket = {latte}
latte.price = 250 # хеш изменился, а в set объект лежит по старому
print(latte in basket)
print(len(basket), list(basket))False
1 [Product('Латте', 250)]В visits — покупатели по порядку визитов за день; один и тот же человек мог представиться по-разному.
Сохраните в n_unique число разных покупателей, а в per_customer — словарь «покупатель → сколько раз
приходил». Используйте класс Customer из прошлого упражнения.
set из визитов.per_customer[c] = per_customer.get(c, 0) + 1.Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
visits = [
Customer("Анна", "+7 900 111-22-33"),
Customer("Борис", "+7 900 222-33-44"),
Customer("Аня", "+7 900 111-22-33"),
Customer("Вера", "+7 900 333-44-55"),
Customer("Анна", "+7 900 111-22-33"),
]
n_unique = ...
per_customer = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
visits = [
Customer("Анна", "+7 900 111-22-33"),
Customer("Борис", "+7 900 222-33-44"),
Customer("Аня", "+7 900 111-22-33"),
Customer("Вера", "+7 900 333-44-55"),
Customer("Анна", "+7 900 111-22-33"),
]
n_unique = len(set(visits))
per_customer = {}
for c in visits:
per_customer[c] = per_customer.get(c, 0) + 1