В прошлых уроках подклассы переопределяли 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(...), и каждый новый
способ доставки требовал бы правки этой цепочки.
Напишите функцию cheapest(options, km): она возвращает тот способ доставки из списка options, у которого
cost(km) меньше всех. Функция не должна зависеть от того, какие классы в списке.
min(options, key=...) с функцией, которая для способа доставки возвращает его cost(km).Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
def cheapest(options, km):
...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
def cheapest(options, km):
return min(options, key=lambda option: option.cost(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, но в чек
попадает так же.
Напишите функцию total(items): сумма атрибутов price всех объектов списка — товаров, сертификатов,
услуг, чего угодно с атрибутом price.
price — класс объекта проверять не нужно.sum(item.price for item in items).Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
def total(items):
...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
def total(items):
return sum(item.price for item in 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 принят
Объявите класс PushChannel: PushChannel("Анна") хранит имя получателя user, а метод send(text)
возвращает строку "пуш для Анна: <текст>". Затем сохраните в sent результат notify для списка из трёх
каналов: sms, email из демонстрации и нового пуш-канала для Анны — с текстом "заказ готов".
SmsChannel, только с другим текстом в send. Функцию notify менять не нужно.Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
class PushChannel:
def __init__(self, user):
...
def send(self, text):
...
sent = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
class PushChannel:
def __init__(self, user):
self.user = user
def send(self, text):
return f"пуш для {self.user}: {text}"
sent = notify([sms, email, PushChannel("Анна")], "заказ готов")hasattr: проверить, есть ли метод
Иногда нужно узнать заранее, умеет ли объект что-то. Встроенная hasattr(объект, "имя") возвращает True,
если у объекта есть атрибут или метод с таким именем:
for thing in [sms, Product("Чай", 120)]:
print(type(thing).__name__, hasattr(thing, "send"))SmsChannel True Product False
Что вернёт hasattr(GiftCertificate(1000), "price") для сертификата из демонстрации о чеке?
hasattr смотрит, есть ли у объекта атрибут, а не от какого он класса: у сертификата price записан в __init__. Поэтому receipt и total с ним и работают.В кассу добавили класс Taxi с методом cost(km). Что нужно изменить в функции cheapest?
cheapest вызывает только cost(km), а у Taxi этот метод есть. Наследование тут не обязательно — достаточно утиной типизации. Так и выглядит полиморфизм: новый класс встраивается, а старый код не меняется.