Наше хранилище данных уже умеет выдавать уникальные идентификаторы, но пока это лишь «голая» база: записи лежат в памяти, и работать с ними приходится вручную. Чтобы проверить, что хранилище годится для настоящей прикладной задачи, мы построим на нём серверную часть простого интернет-магазина: каталог товаров и корзину покупателя.
Если писать такой код «в лоб», быстро возникают проблемы:
- записи о товарах разбросаны по программе, и каждый кусок кода сам решает, как их создавать, менять и удалять, — в итоге в каталоге появляются дубликаты и «осиротевшие» идентификаторы;
- деньги хранят во
float, и суммы вроде0.1 + 0.2начинают расходиться с копейками, а в бухгалтерии такое недопустимо; - товар «переезжает» между складом и корзиной в два отдельных действия, и если одно из них не выполнилось, товар пропадает или появляется из воздуха.
Именно эти вопросы решают классические CRUD-операции (create, read, update,
delete) над единым хранилищем, аккуратная работа с ценой через Decimal и
операции корзины, которые сохраняют общее количество товара. Их нам и предстоит
написать. Позже на этих функциях мы построим интерфейс работы с нашей базой.
Нужно реализовать серверную часть простого интернет-магазина: хранилище товаров, операции CRUD над ним и работу с корзиной покупателя. Настоящая база данных не используется — хранилище это список кортежей в оперативной памяти.
Решение состоит из четырёх модулей:
storage.py— форма записи о товаре и связанные с ней константы;utils.py— округление денежных сумм;crud.py— создание, чтение, изменение и удаление записей о товарах;cart.py— перенос товаров между хранилищем и корзиной.
Все функции, кроме find_cart_line и change_cart_quantity, на неуспешном пути
ничего не бросают: печатают сообщение об ошибке в stdout и возвращают None.
Запись о товаре (Product) — кортеж из четырёх полей, перечисленных по порядку:
| Поле | Тип | Что хранит |
|---|---|---|
| идентификатор | int |
уникальный положительный ключ, назначается при создании записи |
| наименование | str |
человекочитаемое имя, уникальное в пределах хранилища |
| цена | Decimal |
цена за единицу, округлённая до фиксированного числа знаков дроби |
| количество | int |
число единиц товара на складе |
Позиции полей задаются константами *_INDEX, чтобы обращение к кортежу не
использовало числовые литералы.
Строка корзины (CartLine) — кортеж (идентификатор товара, количество). В
корзине не более одной строки на каждый товар.
Определите:
Product— псевдоним типаtuple[int, str, Decimal, int].PRODUCT_ID_INDEX,NAME_INDEX,PRICE_INDEX,QUANTITY_INDEX— позиции полей в кортежеProduct:0,1,2,3. Каждая аннотированаtyping.Final.PRODUCT_ID_MIN: Final[int] = 1— идентификатор, который получает первый товар, добавленный в пустое хранилище.
PRICE_PRECISION: Final[int] = 2— число знаков после запятой, до которого округляется каждая сохраняемая цена.PRICE_STEP: Final— шаг квантования. Вычисляется изPRICE_PRECISION(Decimal(1).scaleb(-PRICE_PRECISION), то естьDecimal("0.01")при значении по умолчанию), а не задаётся числом напрямую; при измененииPRICE_PRECISIONшаг меняется согласованно.
- Вход:
price— произвольная денежная сумма типаDecimal. - Выход: та же сумма, приведённая к
PRICE_PRECISIONзнакам после запятой; ровно половина шага округляется вверх (режимROUND_HALF_UP).
Примеры: Decimal("12.999") → Decimal("13.00"),
Decimal("2.3451") → Decimal("2.35"), Decimal("2.3449") → Decimal("2.34"),
Decimal("5") → Decimal("5.00") (число знаков дроби в результате всегда равно
PRICE_PRECISION).
Каждая функция получает хранилище storage: list[Product] первым параметром и
меняет его на месте.
- Вход: хранилище.
- Выход: если хранилище пустое —
PRODUCT_ID_MIN; иначе — наибольший идентификатор среди записей плюс1. Пропуски в нумерации (например, оставшиеся после удаления) не переиспользуются — берётся именно максимум.
- Вход:
fields— кортеж(наименование, цена, количество);ценатипаDecimal. - Выход: сгенерированный идентификатор новой записи, либо
None.
Поведение.
- Если в хранилище уже есть запись с таким же наименованием — новая запись не
создаётся, хранилище не меняется, печатается сообщение об ошибке, содержащее
это наименование, возвращается
None. (Например:product name '<имя>' is already taken.) - Иначе идентификатор вычисляется через
generate_product_id, цена приводится черезnormalize_price, кортеж(идентификатор, наименование, нормализованная цена, количество)добавляется в хранилище, возвращается идентификатор.
- Вход: целочисленный идентификатор.
- Выход: запись с этим идентификатором, либо
None. Если записи нет — печатается сообщение об ошибке, содержащее переданный идентификатор (например,no product with id <id>).
update_product(storage: list[Product], product_id: int, fields: tuple[str, Decimal, int]) -> Product | None
- Вход: идентификатор и кортеж
(наименование, цена, количество)с новыми значениями полей. - Выход: изменённая запись
(идентификатор, наименование, нормализованная цена, количество)— сам идентификатор сохраняется прежним, остальные три поля заменяются, цена приводится черезnormalize_price. Если записи с таким идентификатором нет — хранилище не меняется, печатается сообщение об ошибке с идентификатором (no product with id <id>), возвращаетсяNone. Уникальность нового наименования не проверяется.
- Вход: идентификатор.
- Выход: тот же идентификатор, если запись была найдена и удалена из
хранилища. Если записи нет — хранилище не меняется, печатается сообщение об
ошибке с идентификатором (
no product with id <id>), возвращаетсяNone.
CartLine— псевдоним типаtuple[int, int]((идентификатор товара, количество)).LINE_PRODUCT_ID_INDEX,LINE_QUANTITY_INDEX— позиции полей строки корзины (0,1), аннотированыtyping.Final.
add_to_cart(storage: list[Product], cart: list[CartLine], product_id: int, quantity: int) -> CartLine | None
Переносит quantity единиц товара product_id со склада в корзину.
- Вход: хранилище, корзина, идентификатор товара, количество для добавления.
- Выход: строка корзины для
product_idпосле добавления, либоNone.
Поведение. Проверки выполняются в этом порядке; при первой же неудаче остальные действия не выполняются:
- Товар ищется в хранилище (через
read_product). Если его нет —read_productпечатает сообщение с идентификатором,add_to_cartвозвращаетNone, больше ничего не меняется. - Иначе количество на складе сравнивается с запрошенным. Если на складе строго
меньше, чем
quantity— печатается сообщение об ошибке, содержащее идентификатор товара (например:not enough stock for product <id>: <склад> available, <quantity> requested), возвращаетсяNone, ничего не меняется. Запрос ровно на всё имеющееся количество (quantity == склад) допустим. - Иначе количество товара в хранилище уменьшается на
quantity(наименование и цена не меняются), а в корзину добавляетсяquantityединиц: если строка для товара уже была — увеличивается её количество, иначе создаётся новая строка. Возвращается получившаяся строка корзины.
remove_from_cart(storage: list[Product], cart: list[CartLine], product_id: int, quantity: int) -> CartLine | None
Переносит quantity единиц товара product_id из корзины обратно на склад.
Принимает тот же набор аргументов, что и add_to_cart.
- Выход: строка корзины для
product_idпосле удаления (количество0означает, что строка удалена), либоNone.
Поведение. Проверки в этом порядке:
- Строка корзины для
product_idищется черезfind_cart_line. Если её нет — печатается сообщение об ошибке, содержащее идентификатор (например:product <id> is not in the cart), возвращаетсяNone, ничего не меняется. - Иначе количество в строке корзины сравнивается с запрошенным. Если в корзине
строго меньше, чем
quantity— печатается сообщение об ошибке с идентификатором (например:cart holds only <в корзине> unit(s) of product <id>, cannot remove <quantity>), возвращаетсяNone, ничего не меняется. - Товар ищется в хранилище через
read_product; если его там нет — возвращаетсяNone(сообщение печатаетread_product). - Иначе количество товара в хранилище увеличивается на
quantity, а из строки корзины вычитаетсяquantity(наименование и цена не меняются); если количество в строке стало0, строка удаляется. Возвращается получившаяся строка корзины.
- Для любой успешно созданной записи
read_product(storage, create_product(storage, (name, price, qty)))возвращает(id, name, normalize_price(price), qty). - После
delete_product(storage, id)вызовread_product(storage, id)возвращаетNone. - Цена в хранилище всегда нормализована:
normalize_priceприменяется при каждом создании и изменении записи. - Сохранение количества: при успешном
add_to_cartилиremove_from_cartсумма «количество товара на складе + количество этого товара в корзине» не меняется. add_to_cart(...)и следующий за нимremove_from_cart(...)с тем жеquantityвозвращают склад и корзину к исходному состоянию.