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

Полиморфизм и утиная типизация

Один и тот же вызов — разное поведение у разных классов. Функция, которой неважен класс объекта, утиная типизация и новый класс, который встраивается в готовый код.

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

В прошлых уроках подклассы переопределяли label, и код вида item.label() давал для каждого класса свою строку. У этого есть имя: полиморфизм (polymorphism) — один вызов, разное поведение у разных классов. Главная польза полиморфизма в том, что код, который вызывает метод, не нужно менять, когда появляется новый класс.

Один вызов — разные классы

Касса предлагает несколько способов доставки. Каждый способ — свой класс со своим расчётом, но у всех есть метод cost(km). Код, который выбирает доставку, не проверяет классы — он просто вызывает cost:

[ ]Три доставки, один вызов
class Pickup:
    name = "самовывоз"

    def cost(self, km):
        return 0


class Courier:
    name = "курьер"

    def cost(self, km):
        return 150 + 30 * km


class Partner:
    name = "служба доставки"

    def cost(self, km):
        return 290


for option in [Pickup(), Courier(), Partner()]:
    print(f"{option.name}: 3 км — {option.cost(3)} ₽, 10 км — {option.cost(10)} ₽")
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
самовывоз: 3 км — 0 ₽, 10 км — 0 ₽
курьер: 3 км — 240 ₽, 10 км — 450 ₽
служба доставки: 3 км — 290 ₽, 10 км — 290 ₽

Без полиморфизма функция выглядела бы как цепочка if isinstance(...) elif isinstance(...), и каждый новый способ доставки требовал бы правки этой цепочки.

[ ]Упражнение 1Самая дешёвая доставка решено

Напишите функцию cheapest(options, km): она возвращает тот способ доставки из списка options, у которого cost(km) меньше всех. Функция не должна зависеть от того, какие классы в списке.

def cheapest(options, km):
    ...

Утиная типизация

Обратите внимание: классы доставки в демонстрации даже не связаны наследованием. Python не проверяет, от какого класса объект, — ему достаточно, что у объекта есть нужный метод. Это называется утиная типизация (duck typing): «если что-то ходит как утка и крякает как утка — это утка».

[ ]Подарочный сертификат в чеке
class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price


class GiftCertificate:  # не наследует от Product
    def __init__(self, amount):
        self.name = f"Сертификат на {amount} ₽"
        self.price = amount


def receipt(items):
    for item in items:
        print(f"{item.name:<24}{item.price:>6} ₽")


receipt([Product("Латте", 220), GiftCertificate(1000)])
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
Латте                      220 ₽
Сертификат на 1000 ₽      1000 ₽

Функции receipt нужны только name и price. Сертификат — не товар и не наследует от Product, но в чек попадает так же.

[ ]Упражнение 2Сумма чего угодно решено

Напишите функцию total(items): сумма атрибутов price всех объектов списка — товаров, сертификатов, услуг, чего угодно с атрибутом price.

def total(items):
    ...

Новый класс в готовом коде

Сила полиморфизма видна, когда к готовому коду добавляют новый класс. Функция notify рассылает сообщение о готовности заказа по всем каналам — ей нужен только метод send(text):

[ ]СМС и почта
class SmsChannel:
    def __init__(self, phone):
        self.phone = phone

    def send(self, text):
        return f"смс на {self.phone}: {text}"


class EmailChannel:
    def __init__(self, address):
        self.address = address

    def send(self, text):
        return f"письмо на {self.address}: {text}"


def notify(channels, text):
    return [channel.send(text) for channel in channels]


sms = SmsChannel("+7 900 111-22-33")
email = EmailChannel("anna@example.com")
for line in notify([sms, email], "заказ №17 принят"):
    print(line)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
смс на +7 900 111-22-33: заказ №17 принят
письмо на anna@example.com: заказ №17 принят
[ ]Упражнение 3Пуш-уведомление решено

Объявите класс PushChannel: PushChannel("Анна") хранит имя получателя user, а метод send(text) возвращает строку "пуш для Анна: <текст>". Затем сохраните в sent результат notify для списка из трёх каналов: sms, email из демонстрации и нового пуш-канала для Анны — с текстом "заказ готов".

class PushChannel:
    def __init__(self, user):
        ...

    def send(self, text):
        ...


sent = ...

hasattr: проверить, есть ли метод

Иногда нужно узнать заранее, умеет ли объект что-то. Встроенная hasattr(объект, "имя") возвращает True, если у объекта есть атрибут или метод с таким именем:

[ ]Есть ли у объекта send
for thing in [sms, Product("Чай", 120)]:
    print(type(thing).__name__, hasattr(thing, "send"))
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
SmsChannel True
Product False
Вопрос

Что вернёт hasattr(GiftCertificate(1000), "price") для сертификата из демонстрации о чеке?

Вопрос

В кассу добавили класс Taxi с методом cost(km). Что нужно изменить в функции cheapest?

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