Нашли ошибку?
Скачать чистый
урок 2 из 4≈ 25 минупражнения: 0 из 3

Равенство: __eq__

__eq__ задаёт, когда два объекта равны по смыслу. == против is, NotImplemented для чужого типа, __hash__ — и почему объект пропал из set.

Python запустится при первом выполнении

В уроке о ссылках == у двух товаров с одинаковыми названием и ценой давал 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__ падает:

[ ]Сравнение со строкой ломает __eq__ошибка: AttributeError
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
[ ]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 (исключение) — это разные вещи.

[ ]Упражнение 1Равные напитки решено

Допишите класс Drink: два напитка равны, если у них совпадают цена и название — без учёта регистра: кассиры пишут то «Латте», то «латте». Сравнение с объектом другого типа возвращает NotImplemented.

class Drink:
    def __init__(self, name, price):
        self.name = name
        self.price = price

    def __eq__(self, other):
        ...

__hash__: объекты в set и ключи dict

set и dict находят элементы не перебором, а по хешу (hash) — числу, которое вычисляет hash(x). Правило: равные объекты обязаны иметь равные хеши, иначе set положит два «равных» товара в разные места. Поэтому, как только в классе появляется __eq__, Python отключает хеш по умолчанию — экземпляр перестаёт быть хешируемым (hashable):

[ ]unhashable type: 'Product'ошибка: TypeError
print(Product.__hash__)  # __eq__ есть — хеш отключён
{latte}
Ошибкасохранённый · нажмите «Выполнить», чтобы получить свой
None
TypeError: cannot use 'Product' as a set element (unhashable type: 'Product')

Чтобы вернуть хеш, объявляют __hash__ и вычисляют его только из атрибутов, которые сравнивает __eq__ — из всех или из части, — но не из других: иначе у равных объектов хеши разойдутся. Проще всего — хеш кортежа из всех сравниваемых атрибутов: hash((self.name, self.price)).

[ ]Product в set
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

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

[ ]Упражнение 2Покупатель по телефону решено

Допишите класс Customer: покупатели равны, если у них один и тот же телефон phone (имя может быть записано по-разному: «Анна» и «Аня»). Сравнение с другим типом — NotImplemented. Добавьте __hash__, чтобы покупателей можно было класть в set и делать ключами dict.

class Customer:
    def __init__(self, name, phone):
        self.name = name
        self.phone = phone

    def __eq__(self, other):
        ...

    def __hash__(self):
        ...

Почему объект пропал из 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)]
[ ]Упражнение 3Визиты покупателей решено

В visits — покупатели по порядку визитов за день; один и тот же человек мог представиться по-разному. Сохраните в n_unique число разных покупателей, а в per_customer — словарь «покупатель → сколько раз приходил». Используйте класс 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 = ...
per_customer = ...

Нашли ошибку?