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

Итоговый проект: скидки, оплата и чек

Вторая часть кассы: скидки и оплата — иерархии с абстрактным классом, чек — dataclass со __str__, касса содержит скидки, выбирает лучшую и ведёт отчёт за день. И что изучать дальше.

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

Модель кассы есть: товары, строки и корзины. Теперь — правила: какую скидку дать, как принять оплату, как напечатать чек и посчитать выручку за день. Каждое правило — отдельный класс или иерархия классов, а касса Register собирает их вместе композицией.

Часть кассы Что это Инструмент
скидки варианты одного поведения абстрактный класс + подклассы, полиморфизм
оплата варианты одного поведения, свои ошибки абстрактный класс, PaymentError
чек данные и текст для печати dataclass, свойство, __str__
касса всё вместе композиция, max(..., key=...), исключения

Что у нас уже есть

Упрощённая модель из первой части: корзина сразу получает строки (каталог здесь не нужен), но умеет то же, что ваша, — subtotal, len и цикл по строкам. Готов и «договор» для оплаты — абстрактный PaymentMethod с наличными Cash, как в мини-проекте модуля 6:

[ ]Товары, строки, корзины, наличные
from abc import ABC, abstractmethod
from dataclasses import dataclass, field


class ShopError(Exception):
    pass


@dataclass(frozen=True)
class Product:
    name: str
    price: int
    category: str


@dataclass
class Line:
    product: Product
    qty: int

    @property
    def cost(self):
        return self.product.price * self.qty


@dataclass
class Cart:
    lines: list = field(default_factory=list)

    def __len__(self):
        return len(self.lines)

    def __iter__(self):
        return iter(self.lines)

    @property
    def subtotal(self):
        return sum(line.cost for line in self.lines)


class PaymentError(ShopError):
    pass


class PaymentMethod(ABC):
    @abstractmethod
    def pay(self, amount):
        ...


class Cash(PaymentMethod):
    def __init__(self, given):
        self.given = given

    def pay(self, amount):
        if self.given < amount:
            raise PaymentError("недостаточно наличных")
        return f"наличные {self.given} ₽, сдача {self.given - amount} ₽"


latte = Product("Латте", 240, "кофе")
croissant = Product("Круассан", 140, "выпечка")
muffin = Product("Маффин", 130, "выпечка")
tea = Product("Чай", 120, "чай")
raf = Product("Раф", 260, "кофе")

sample = Cart([Line(latte, 3), Line(croissant, 2)])
print(sample.subtotal, len(sample), [line.cost for line in sample])
print(Cash(1000).pay(900))
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
1000 2 [720, 280]
наличные 1000 ₽, сдача 100 ₽

Шаг 1. Скидки

[ ]Упражнение 1Абстрактная скидка решено

Объявите абстрактный класс Discount: __init__(self, title) записывает название акции в title, str(discount) возвращает его, абстрактный метод amount(cart) — сколько рублей скидка даёт корзине.

class Discount:
    def __init__(self, title):
        ...

    def __str__(self):
        ...

    def amount(self, cart):
        ...

[ ]Упражнение 2Процент и категория решено

Объявите два подкласса Discount. PercentDiscount(10) — название "скидка 10 %", скидка — 10 % от subtotal корзины. CategoryDiscount("выпечка", 20) — название "выпечка -20 %", скидка — 20 % от стоимости только тех строк корзины, у товара которых такая категория. Обе скидки округляются до целых.

class PercentDiscount(Discount):
    def __init__(self, percent):
        ...

    def amount(self, cart):
        ...


class CategoryDiscount(Discount):
    def __init__(self, category, percent):
        ...

    def amount(self, cart):
        ...

Шаг 2. Оплата

[ ]Упражнение 3Карта решено

Наличные Cash уже есть. Объявите второй способ оплаты — подкласс Card класса PaymentMethod: Card(2000) хранит остаток на счёте в balance, pay(amount) списывает сумму и возвращает "карта: 664 ₽", а если денег меньше — выбрасывает PaymentError("недостаточно средств"), и баланс не меняется.

class Card(PaymentMethod):
    def __init__(self, balance):
        ...

    def pay(self, amount):
        ...

Шаг 3. Чек

[ ]Упражнение 4Receipt решено

В заготовке — dataclass Receipt с полями number, subtotal, discount (название акции), saved (скидка в рублях), payment (строка оплаты) и свойством total. Допишите __str__: текст чека — пять строк:

Чек №1
Сумма: 1000 ₽
Скидка «скидка 10 %»: -100 ₽
К оплате: 900 ₽
Оплата: наличные 1000 ₽, сдача 100 ₽
@dataclass
class Receipt:
    number: int
    subtotal: int
    discount: str
    saved: int
    payment: str

    @property
    def total(self):
        return self.subtotal - self.saved

    def __str__(self):
        ...

Шаг 4. Касса

[ ]Упражнение 5Register решено

Объявите класс Register: Register([скидка, скидка, …]) хранит доступные скидки и список выданных чеков receipts (у новой кассы — пустой).

  • best_discount(cart) — скидка, которая даёт корзине больше всего рублей;
  • checkout(cart, method) — провести покупку: пустая корзина (subtotal равен 0) — ShopError("пустая корзина"); иначе выбрать лучшую скидку, оплатить method сумму к оплате (ошибка оплаты уходит дальше, чек не создаётся), создать Receipt со следующим номером (1, 2, 3… по выданным чекам), сохранить его в receipts и вернуть.
class Register:
    def __init__(self, discounts):
        ...

    def best_discount(self, cart):
        ...

    def checkout(self, cart, method):
        ...

Шаг 5. День на кассе

[ ]Упражнение 6Отчёт за день решено

Касса работает со скидками PercentDiscount(10) и CategoryDiscount("выпечка", 20). Проведите покупки из queue (пары «корзина, способ оплаты») через одну кассу register. Ошибки кассы перехватывайте и считайте: failed — сколько покупок не прошло. Сохраните в revenue выручку — сумму total всех чеков, а в biggest — номер чека с наибольшей суммой к оплате.

card = Card(2000)
queue = [
    (Cart([Line(latte, 3), Line(croissant, 2)]), Cash(1000)),
    (Cart([Line(croissant, 3), Line(muffin, 2), Line(tea, 1)]), card),
    (Cart([Line(raf, 1)]), Cash(200)),
    (Cart(), Cash(500)),
    (Cart([Line(tea, 2), Line(muffin, 1)]), card),
]
register = ...
failed = ...
revenue = ...
biggest = ...

Итог

[ ]Чеки за день
for receipt in register.receipts:
    print(receipt)
    print()
print(f"не прошло покупок: {failed}; выручка: {revenue} ₽; самый большой чек — №{biggest}")
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
Чек №1
Сумма: 1000 ₽
Скидка «скидка 10 %»: -100 ₽
К оплате: 900 ₽
Оплата: наличные 1000 ₽, сдача 100 ₽

Чек №2
Сумма: 800 ₽
Скидка «выпечка -20 %»: -136 ₽
К оплате: 664 ₽
Оплата: карта: 664 ₽

Чек №3
Сумма: 370 ₽
Скидка «скидка 10 %»: -37 ₽
К оплате: 333 ₽
Оплата: карта: 333 ₽

не прошло покупок: 2; выручка: 1897 ₽; самый большой чек — №1
Итог появится, когда решите все упражнения урока, — в нём ответы. Ячейку можно и выполнить.

Из пяти покупок прошли три: в первой лучшей оказалась скидка 10 % (100 ₽ против 56 ₽ на выпечку), во второй — скидка на выпечку (136 ₽). Покупка рафа не прошла — 200 ₽ наличными меньше 234 ₽ к оплате, а пустую корзину касса не провела вовсе. Выручка — 1897 ₽.

Выводы с ответами появятся, когда решите все упражнения урока.

Что получилось

Касса «Кофейной лавки» — это дюжина небольших классов, и каждый отвечает за одно: Product и Line хранят данные, Cart — строки, скидки и способы оплаты — варианты поведения за общим абстрактным интерфейсом, Receipt — данные чека и его текст, Register связывает всё вместе. Новая акция или новый способ оплаты — это новый подкласс; касса и отчёт не меняются. Это и есть цель объектно-ориентированного проектирования.

Что дальше

Курс закончен, но у ООП в Python есть куда расти. Темы в порядке пользы:

  • Аннотации типов и typing.Protocol. Аннотации можно писать не только в dataclass, но и в любых методах, а программы-проверщики (mypy, pyright) найдут ошибки до запуска. Protocol описывает «утиный» интерфейс без наследования — как способы доставки из модуля 5.
  • Тесты. Проверки в упражнениях курса — это тесты. В настоящих проектах их пишут с библиотекой pytest: функции test_… с assert, как здесь.
  • Паттерны проектирования. Шаблонный метод, стратегия (скидки и способы оплаты — это она), фабрика (альтернативные конструкторы) — у типовых решений есть имена; книги о паттернах помогают узнавать их в чужом коде.
  • __slots__, __getattr__, дескрипторы. Как property устроено изнутри и как сэкономить память на миллионе экземпляров. Это продвинутые темы, к ним возвращаются по необходимости.
  • Классы библиотек. Попробуйте прочитать устройство знакомых объектов: pathlib.Path перегружает оператор / (Path("data") / "orders.csv"), а DataFrame в pandas — контейнер с __getitem__, __len__ и __iter__. Теперь вы понимаете, почему df["price"], len(df) и for column in df работают.

Задачи для практики по всем темам курса появятся в разделе «Задачи», а справочник по ООП — во вкладке «Справочник».

Практика по модулю

Задачи по ООП в Python для самостоятельной практики появятся в разделе «Задачи → ООП в Python».

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