У «Кофейной лавки» три вида акций: процент от суммы, фиксированная сумма и «каждый n-й напиток в подарок».
Касса должна уметь применить любую из них — и выбрать для покупателя самую выгодную. Если писать это
цепочкой if, каждая новая акция будет требовать правки кассы. Вместо этого сделаем иерархию классов:
у всех скидок один метод amount(cart) — сколько рублей скидка даёт для корзины, — а касса просто вызывает его.
Что у нас уже есть
Товар и корзина из прошлых модулей: subtotal — сумма корзины без скидок.
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __repr__(self):
return f"Product({self.name!r}, {self.price})"
class Cart:
def __init__(self, items=None):
self.items = list(items) if items is not None else []
def add(self, product):
self.items.append(product)
@property
def subtotal(self):
return sum(p.price for p in self.items)
cart = Cart([Product("Латте", 220), Product("Круассан", 140)])
print(cart.items, cart.subtotal)[Product('Латте', 220), Product('Круассан', 140)] 360Шаг 1. Базовая скидка
Допишите класс Discount: Discount("без скидки") хранит название акции в атрибуте title, str(discount)
возвращает это название, а метод amount(cart) возвращает 0 — базовая «скидка» ничего не скидывает.
Подклассы переопределят amount.
__str__ возвращает self.title; amount — return 0.Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
class Discount:
def __init__(self, title):
...
def __str__(self):
...
def amount(self, cart):
...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
class Discount:
def __init__(self, title):
self.title = title
def __str__(self):
return self.title
def amount(self, cart):
return 0Шаг 2. Процент и фиксированная сумма
Подклассы наследуют от Discount всё, что есть в шаге 1, и переопределяют только amount. Название акции
каждый подкласс собирает сам и передаёт в __init__ базового класса.
Объявите подкласс PercentDiscount: PercentDiscount(10) — скидка 10 %, процент хранится в атрибуте
percent, название — "скидка 10 %". amount(cart) — процент от cart.subtotal, округлённый до целых.
super().__init__(f"скидка {percent} %"), затем self.percent = percent.Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
class PercentDiscount(Discount):
def __init__(self, percent):
...
def amount(self, cart):
...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
class PercentDiscount(Discount):
def __init__(self, percent):
super().__init__(f"скидка {percent} %")
self.percent = percent
def amount(self, cart):
return round(cart.subtotal * self.percent / 100)Объявите подкласс FixedDiscount: FixedDiscount(150) — минус 150 ₽, сумма в атрибуте value, название —
"минус 150 ₽". Скидка не может быть больше суммы корзины: для корзины на 100 ₽ amount — 100.
min(self.value, cart.subtotal).Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
class FixedDiscount(Discount):
def __init__(self, value):
...
def amount(self, cart):
...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
class FixedDiscount(Discount):
def __init__(self, value):
super().__init__(f"минус {value} ₽")
self.value = value
def amount(self, cart):
return min(self.value, cart.subtotal)Шаг 3. Каждый n-й в подарок
Объявите подкласс BuyNGetOne: BuyNGetOne("Латте", 3) — каждый третий латте в подарок, название —
"каждый 3-й Латте в подарок", атрибуты name и n. amount(cart): сколько товаров с таким названием
в корзине, столько раз по n целиком — столько бесплатных; скидка — их число, умноженное на цену этого
товара. Нет товара в корзине — 0.
[p for p in cart.items if p.name == self.name]; бесплатных — len(...) // self.n.Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
class BuyNGetOne(Discount):
def __init__(self, name, n):
...
def amount(self, cart):
...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
class BuyNGetOne(Discount):
def __init__(self, name, n):
super().__init__(f"каждый {n}-й {name} в подарок")
self.name = name
self.n = n
def amount(self, cart):
same = [p for p in cart.items if p.name == self.name]
if not same:
return 0
return len(same) // self.n * same[0].priceШаг 4. Касса
Напишите две функции. to_pay(cart, discount) — сумма к оплате: subtotal корзины минус скидка.
best_discount(cart, discounts) — та скидка из списка, которая для этой корзины даёт больше всего рублей.
Ни одна функция не должна проверять класс скидки.
discount.amount(cart) — это и есть полиморфизм.Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
def to_pay(cart, discount):
...
def best_discount(cart, discounts):
...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
def to_pay(cart, discount):
return cart.subtotal - discount.amount(cart)
def best_discount(cart, discounts):
return max(discounts, key=lambda d: d.amount(cart))Шаг 5. Корзина Анны
В корзине Анны три латте, два круассана и чай. Для трёх акций из списка offers сохраните в prices словарь
«название акции → сумма к оплате», а в best — название самой выгодной акции.
str(discount); сумма — to_pay(anna, discount).Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
anna = Cart([Product("Латте", 220)] * 3 + [Product("Круассан", 140)] * 2 + [Product("Чай", 120)])
offers = [PercentDiscount(10), FixedDiscount(150), BuyNGetOne("Латте", 3)]
prices = ...
best = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
anna = Cart([Product("Латте", 220)] * 3 + [Product("Круассан", 140)] * 2 + [Product("Чай", 120)])
offers = [PercentDiscount(10), FixedDiscount(150), BuyNGetOne("Латте", 3)]
prices = {str(d): to_pay(anna, d) for d in offers}
best = str(best_discount(anna, offers))Итог
print(f"корзина Анны без скидки: {anna.subtotal} ₽")
for offer in offers:
print(f"{offer}: скидка {offer.amount(anna)} ₽, к оплате {to_pay(anna, offer)} ₽")
print("лучшая акция:", best)корзина Анны без скидки: 1060 ₽ скидка 10 %: скидка 106 ₽, к оплате 954 ₽ минус 150 ₽: скидка 150 ₽, к оплате 910 ₽ каждый 3-й Латте в подарок: скидка 220 ₽, к оплате 840 ₽ лучшая акция: каждый 3-й Латте в подарок
Без скидки корзина Анны стоит 1060 ₽. Скидка 10 % даёт 106 ₽, «минус 150 ₽» — 150 ₽, а третий латте в подарок — 220 ₽: эта акция выгоднее всех, Анна платит 840 ₽.
Посмотрите на функции кассы: в них нет ни одного isinstance. Новая акция — скажем, «скидка на выпечку» —
это новый подкласс с методом amount, а to_pay и best_discount не меняются. Но базовый Discount пока
можно создать и «забыть» переопределить в подклассе amount — касса молча посчитает скидку 0. Как запретить
такое — в следующем модуле.