Перед тем как писать код, хороший разработчик внимательно читает ТЗ. Прочитай его целиком, прежде чем переходить к следующей части.
Нам поступил заказ на разработку консольного прототипа корзины покупок для интернет-магазина.
Заказчик хочет систему, в которой можно:
- добавлять товары в корзину (название, цена, количество);
- если товар с таким названием уже есть в корзине — количество должно суммироваться, а не создавать дубликат позиции;
- удалять товар из корзины по названию;
- смотреть список всех товаров в корзине;
- смотреть итоговое количество товаров и итоговую стоимость корзины;
- сохранять текущую корзину в файл и загружать её обратно.
Ключевое требование заказчика: никакие некорректные данные не должны попадать в корзину. Ни в коем случае в системе не должно быть возможности создать товар с отрицательной ценой, пустым названием или дробным количеством — вне зависимости от того, откуда пришли данные: из консоли или из ранее сохранённого файла.
- Название товара — непустая строка.
- Цена товара — число (целое или дробное), строго больше нуля.
- Количество товара — целое число, не меньшее нуля (ноль допустим — это означает «нет в наличии», а вот отрицательного количества быть не может).
- При повторном добавлении товара с уже существующим названием в корзине количество суммируется с уже имеющимся, новая отдельная позиция не создаётся.
- Удалить можно только тот товар, который действительно есть в корзине — попытка удалить отсутствующий товар должна завершаться понятной ошибкой, а не молча ничего не делать.
- Список товаров, возвращаемый наружу из корзины, не должен позволять менять содержимое корзины в обход её собственных методов (
add_product/remove_product). - Итоговая стоимость корзины — это не хранимое, а вычисляемое значение: она всегда должна быть согласована с фактическим содержимым корзины на текущий момент, и её невозможно случайно «забыть» пересчитать.
| Требование | Детали |
|---|---|
| Язык | Python 3.10+ |
| Хранилище данных | JSON-файл (data/cart.json) |
| Интерфейс | консоль (input/print) |
| Сторонние библиотеки | не использовать (только стандартная библиотека) |
Здесь описана точная архитектура, которую нужно реализовать. Отступать от неё без согласования с преподавателем нельзя — архитектура специально спроектирована так, чтобы закрепить пройденные темы Модуля 2 (инкапсуляция,
property, дескрипторы).
online_shop/
│
├── data/ # папка создаётся автоматически при первом запуске
│ └── cart.json # сохранённая корзина
│
├── models/
│ ├── __init__.py
│ ├── descriptors.py # NonEmptyString, PositiveNumber, NonNegativeInteger
│ └── product.py # класс Product
│
├── cart/
│ ├── __init__.py
│ └── cart.py # класс Cart
│
├── storage/
│ ├── __init__.py
│ └── json_storage.py # класс JsonStorage
│
└── main.py # точка входа, консольное меню
В этом файле нужно реализовать три отдельных класса-дескриптора, каждый из которых инкапсулирует одну проверку и может быть переиспользован для разных атрибутов и разных классов, не будучи привязанным к конкретному полю конкретного класса.
Каждый дескриптор должен реализовывать __set_name__, __get__ и __set__ (это data descriptor — чтение и запись должны быть под контролем).
Проверяет, что присваиваемое значение — непустая строка. При нарушении — ValueError.
Проверяет, что присваиваемое значение — число (int или float), строго больше нуля. При нарушении — ValueError.
Проверяет, что присваиваемое значение — целое число, не меньшее нуля. При нарушении — ValueError.
Почему именно дескрипторы, а не
property? Как разбиралось на Уроке 9, если бы каждая из этих трёх проверок писалась через@property/@setterотдельно для каждого поля классаProduct, пришлось бы трижды повторить практически одинаковую структуру, отличающуюся только условием проверки. Дескриптор пишется один раз и переиспользуется для любого количества полей — в том числе, если магазин в будущем обрастёт новыми классами (например, классом заказа или классом скидки), эти же три дескриптора можно будет применить и там, ничего не переписывая заново.
Представляет товар в каталоге магазина.
Атрибуты:
| Атрибут | Тип | Описание |
|---|---|---|
name |
str |
название товара — реализовано через дескриптор NonEmptyString |
price |
int | float |
цена товара — реализовано через дескриптор PositiveNumber |
quantity |
int |
количество товара — реализовано через дескриптор NonNegativeInteger |
Методы:
-
__init__(name, price, quantity)— присваивает три атрибута через обычное присваивание (self.name = nameи так далее). Само присваивание автоматически пройдёт через дескрипторы и выполнит валидацию — отдельно писать проверки в__init__не нужно. -
to_dict()— возвращает словарь{"name": ..., "price": ..., "quantity": ...}для последующего сохранения в JSON. -
from_dict(data)— метод класса (альтернативный конструктор, как на Уроке 6). Принимает словарь такого же вида, что возвращаетto_dict(), и создаёт из него новый объектProductчерезcls(...).
Важно: не пытайся обращаться к
product._name,product._priceилиproduct._quantityнапрямую откуда-либо, кроме как через официальные атрибутыname/price/quantity. Технически такое обращение возможно (мы разбирали на Уроке 7, что одно подчёркивание — это соглашение, а не запрет), но оно полностью обходит валидацию дескриптора, ради которой всё и было спроектировано.
Представляет корзину покупок.
Атрибуты:
| Атрибут | Тип | Описание |
|---|---|---|
_items |
list[Product] |
protected-список товаров в корзине |
Методы:
-
__init__()— создаёт пустой список_items. -
add_product(product)— добавляет товар:- если товар с таким же
nameуже есть в корзине — увеличивает егоquantityнаquantityдобавляемого товара (то есть суммирует количество, а не создаёт вторую позицию); - если такого товара ещё нет — добавляет переданный объект
Productв список.
- если товар с таким же
-
remove_product(name)— удаляет товар из корзины по названию. Если товара с таким названием нет — выбрасываетValueError. -
get_products()— возвращает копию списка_items(черезlist(self._items)), а не сам список, — чтобы внешний код не мог изменить содержимое корзины в обходadd_product/remove_product. -
_find_by_name(name)— protected вспомогательный метод, возвращающий объектProductс заданным именем илиNone, если такого нет. Используется внутриadd_productиremove_product, чтобы не дублировать поиск.
Свойства (@property):
-
total_price— только для чтения. Возвращает суммуprice * quantityпо всем товарам корзины. Значение не хранится, а вычисляется заново при каждом обращении — точно так же, как вычислялась площадь в примере с классомCircleна Уроке 8. -
total_items— только для чтения. Возвращает суммуquantityпо всем товарам корзины.
Отвечает за сохранение и загрузку корзины в JSON-файл.
Атрибуты:
| Атрибут | Тип | Описание |
|---|---|---|
_filepath |
str |
путь к JSON-файлу |
Методы:
-
__init__(filepath)— сохраняет путь. Если папка, в которой должен лежать файл, не существует — создаёт её. Если сам файл не существует — создаёт его с содержимым{"items": []}. -
_read()— protected-метод. Читает и возвращает содержимое JSON-файла в виде словаря. -
_write(data)— protected-метод. Записывает переданный словарь в JSON-файл. -
save_cart(cart)— сохраняет содержимое переданной корзины в файл: получает список товаров черезcart.get_products(), преобразует каждый товар в словарь черезproduct.to_dict(), записывает результат в формате{"items": [...]}. -
load_cart(cart)— читает файл, восстанавливает каждый товар черезProduct.from_dict(...)и добавляет его в переданную корзину черезcart.add_product(...)(то есть если корзина уже что-то содержала — количества корректно суммируются тем же механизмом, что и при обычном добавлении).
main.py
└── создаёт Cart и JsonStorage
├── Cart хранит список объектов Product
│ └── Product.name / .price / .quantity — реализованы через дескрипторы
│ (NonEmptyString, PositiveNumber, NonNegativeInteger из models/descriptors.py)
└── JsonStorage читает/пишет Product через to_dict() / from_dict()
Точка входа в приложение. Здесь находится только консольный интерфейс — никакой бизнес-логики (валидация — внутри дескрипторов, работа с корзиной — внутри Cart, работа с файлом — внутри JsonStorage).
=== Интернет-магазин: корзина ===
1. Добавить товар
2. Удалить товар
3. Показать список товаров
4. Показать общую стоимость корзины
5. Сохранить корзину в файл
6. Загрузить корзину из файла
0. Выйти
# main.py — структура (не финальный код, только скелет)
from models.product import Product
from cart.cart import Cart
from storage.json_storage import JsonStorage
def main():
cart = Cart()
storage = JsonStorage("data/cart.json")
while True:
# показать меню
# обработать выбор пользователя
# вызвать методы cart.* / storage.*, обернув создание Product в try/except ValueError
pass
if __name__ == "__main__":
main()Конкретная реализация всегда зависит от разработчика.
from models.product import Product
from cart.cart import Cart
from storage.json_storage import JsonStorage
def show_menu():
print("\n=== Интернет-магазин: корзина ===")
print("1. Добавить товар")
print("2. Удалить товар")
print("3. Показать список товаров")
print("4. Показать общую стоимость корзины")
print("5. Сохранить корзину в файл")
print("6. Загрузить корзину из файла")
print("0. Выйти")
return input("Ваш выбор: ").strip()
def main():
cart = Cart()
storage = JsonStorage("data/cart.json")
while True:
choice = show_menu()
if choice == "1":
name = input("Название товара: ").strip()
try:
price = float(input("Цена: "))
quantity = int(input("Количество: "))
product = Product(name, price, quantity)
cart.add_product(product)
print(f"Товар '{name}' добавлен в корзину")
except ValueError as e:
print(f"Ошибка: {e}")
elif choice == "2":
name = input("Название товара для удаления: ").strip()
try:
cart.remove_product(name)
print(f"Товар '{name}' удалён из корзины")
except ValueError as e:
print(f"Ошибка: {e}")
elif choice == "3":
products = cart.get_products()
if not products:
print("Корзина пуста")
for product in products:
print(f"{product.name}: {product.price:.2f} руб. x {product.quantity} шт.")
elif choice == "4":
print(f"Итого товаров: {cart.total_items}")
print(f"Итоговая стоимость: {cart.total_price:.2f} руб.")
elif choice == "5":
storage.save_cart(cart)
print("Корзина сохранена в файл")
elif choice == "6":
storage.load_cart(cart)
print("Корзина загружена из файла")
elif choice == "0":
print("Работа программы завершена")
break
else:
print("Неверный выбор.")
if __name__ == "__main__":
main()Ориентировочное время выполнения: 2–3 академических часа.