{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Мини-проект: ценник\n",
    "\n",
    "Класс Product для меню «Кофейной лавки»: цена за 100 мл, цена со скидкой и строка ценника, каталог из экземпляров и самый выгодный напиток.\n",
    "\n",
    "Курс ООП в Python · урок `oop-p1-price-card` · [урок на сайте](https://ds-edu-trainer.vercel.app/courses/oop/p1-price-card).\n",
    "\n",
    "Выполняйте ячейки по порядку (Shift+Enter), начиная с первой — она готовит урок. После каждого упражнения идёт\n",
    "ячейка проверки: она печатает ✓ / ✗ и итог."
   ],
   "id": "intro"
  },
  {
   "cell_type": "code",
   "metadata": {
    "cellView": "form",
    "jupyter": {
     "source_hidden": true
    }
   },
   "source": [
    "# @title Подготовка урока — выполните эту ячейку первой\n",
    "# Проверяет версии пакетов, записывает файлы данных в папку data/ и определяет _check — проверку упражнений.\n",
    "import base64, os, zlib\n",
    "\n",
    "import sys\n",
    "\n",
    "if sys.version_info < (3, 10):\n",
    "    print(f\"Урок написан для Python 3.10 и новее, у вас {sys.version.split()[0]} — часть кода может не работать\")\n",
    "\n",
    "# ─── Проверка упражнений: тот же раннер, что у задач на сайте ───\n",
    "# ─── Запуск тестов ───────────────────────────────────────────────────────────\n",
    "\n",
    "_TIMEOUT = 2.0  # секунд на тест; отдельный тест может задать свой: test_x.timeout = 10\n",
    "\n",
    "\n",
    "def _call_with_timeout(target, seconds: float) -> bool:\n",
    "    \"\"\"Запускает target() в отдельном потоке. Возвращает False, если за seconds он не завершился.\"\"\"\n",
    "    import sys\n",
    "    import threading\n",
    "\n",
    "    if sys.platform == \"emscripten\":  # Python в браузере: потоков нет, таймаут обеспечивает страница\n",
    "        target()\n",
    "        return True\n",
    "    # Большой стек нужен, чтобы глубокая рекурсия давала RecursionError, а не аварийное завершение Python.\n",
    "    for megabytes in (256, 64, 0):\n",
    "        try:\n",
    "            threading.stack_size(megabytes * 1024 * 1024)\n",
    "            worker = threading.Thread(target=target, daemon=True)\n",
    "            worker.start()\n",
    "        except (ValueError, RuntimeError, MemoryError):\n",
    "            continue\n",
    "        worker.join(seconds)\n",
    "        return not worker.is_alive()\n",
    "    target()  # потоки недоступны — запускаем без таймаута\n",
    "    return True\n",
    "\n",
    "\n",
    "def _test_info(fn) -> dict:\n",
    "    \"\"\"Описание теста, его лимит времени и сообщение на случай зависания.\"\"\"\n",
    "    limit = getattr(fn, \"timeout\", _TIMEOUT)\n",
    "    hint = getattr(fn, \"timeout_hint\", \"\")\n",
    "    message = (\n",
    "        f\"тест не завершился за {limit:g} с — возможно, бесконечный цикл или рекурсия: \"\n",
    "        \"проверьте, что границы/аргументы изменяются на каждом шаге\" + (f\". {hint}\" if hint else \"\")\n",
    "    )\n",
    "    return {\"title\": (fn.__doc__ or fn.__name__).strip(), \"timeout\": limit, \"timeout_message\": message}\n",
    "\n",
    "\n",
    "def _error_result(title: str, error: BaseException, user_file: str | None) -> dict:\n",
    "    \"\"\"Результат упавшего теста: статус, сообщение, тип исключения и строка в коде решения.\"\"\"\n",
    "    line = None\n",
    "    if user_file:  # самый глубокий кадр в коде решения; ошибка внутри самих тестов — без номера строки\n",
    "        tb = error.__traceback__\n",
    "        while tb is not None:\n",
    "            if tb.tb_frame.f_code.co_filename == user_file:\n",
    "                line = tb.tb_lineno\n",
    "            tb = tb.tb_next\n",
    "    status, message = \"error\", f\"{type(error).__name__}: {error}\"\n",
    "    if isinstance(error, NotImplementedError):\n",
    "        status, message, line = \"not_written\", \"функция ещё не написана\", None\n",
    "    elif isinstance(error, AssertionError):\n",
    "        status, message = \"failed\", str(error) or \"проверка не прошла\"\n",
    "    elif isinstance(error, RecursionError):\n",
    "        message = \"RecursionError: слишком глубокая рекурсия — проверьте базовый случай и то, что задача уменьшается\"\n",
    "    elif isinstance(error, MemoryError) or (isinstance(error, OverflowError) and \"index-sized\" in str(error)):\n",
    "        message = f\"{type(error).__name__}: не хватило памяти — возможно, создаётся слишком большая структура данных\"\n",
    "    return {\"title\": title, \"status\": status, \"message\": message, \"error_type\": type(error).__name__, \"line\": line}\n",
    "\n",
    "\n",
    "def _run_tests(verbose: bool = True, on_start=None, on_result=None, user_file: str | None = None) -> list[dict]:\n",
    "    \"\"\"Запускает тесты задачи (_TESTS — функции из tests.py) по порядку, каждый — с таймаутом.\n",
    "\n",
    "    Возвращает список результатов: {\"title\", \"status\", \"message\", \"error_type\", \"line\"}, где status —\n",
    "    passed · failed · error · not_written · timeout · not_run. Если тест завис, при verbose=True печатает\n",
    "    итог и завершает процесс: зависший поток остановить нельзя. on_start(i) и on_result(i, результат)\n",
    "    вызываются по мере выполнения; user_file — имя файла с кодом решения для номеров строк в ошибках.\n",
    "    \"\"\"\n",
    "    import os\n",
    "    import sys\n",
    "\n",
    "    if verbose:\n",
    "        try:  # ✓ и кириллица в консоли Windows со старой кодировкой\n",
    "            sys.stdout.reconfigure(encoding=\"utf-8\")\n",
    "        except Exception:  # noqa: BLE001 — reconfigure может не быть (перенаправленный вывод, браузер)\n",
    "            pass\n",
    "\n",
    "    tests = list(_TESTS)\n",
    "    results: list[dict] = []\n",
    "    not_written = hung = False\n",
    "    for index, fn in enumerate(tests):\n",
    "        info = _test_info(fn)\n",
    "        outcome: dict = {}\n",
    "\n",
    "        def target(fn=fn, outcome=outcome):\n",
    "            try:\n",
    "                fn()\n",
    "            except BaseException as e:\n",
    "                outcome[\"error\"] = e\n",
    "\n",
    "        if on_start:\n",
    "            on_start(index)\n",
    "        if not _call_with_timeout(target, info[\"timeout\"]):\n",
    "            results.append({\"title\": info[\"title\"], \"status\": \"timeout\", \"message\": info[\"timeout_message\"], \"error_type\": None, \"line\": None})\n",
    "            for other in tests[index + 1:]:\n",
    "                results.append({\"title\": _test_info(other)[\"title\"], \"status\": \"not_run\", \"message\": \"не запускался: предыдущий тест завис\", \"error_type\": None, \"line\": None})\n",
    "            hung = True\n",
    "            break\n",
    "        error = outcome.get(\"error\")\n",
    "        if error is None:\n",
    "            result = {\"title\": info[\"title\"], \"status\": \"passed\", \"message\": \"\", \"error_type\": None, \"line\": None}\n",
    "        else:\n",
    "            result = _error_result(info[\"title\"], error, user_file)\n",
    "            not_written = not_written or result[\"status\"] == \"not_written\"\n",
    "        results.append(result)\n",
    "        if on_result:\n",
    "            on_result(index, result)\n",
    "\n",
    "    if not_written:  # пока функция не написана, «даром» пройденные тесты не засчитываются\n",
    "        for r in results:\n",
    "            if r[\"status\"] == \"passed\":\n",
    "                r.update(status=\"not_written\", message=\"не засчитан, пока функция не написана\")\n",
    "    for r in results:\n",
    "        if len(r[\"message\"]) > 300:\n",
    "            r[\"message\"] = r[\"message\"][:300] + \"…\"\n",
    "\n",
    "    if verbose:\n",
    "        if not_written:\n",
    "            print(\"Функция ещё не написана — замените raise NotImplementedError своим кодом.\\n\")\n",
    "        for r in results:\n",
    "            mark = {\"passed\": \"✓\", \"not_run\": \"–\"}.get(r[\"status\"], \"✗\")\n",
    "            print(f\"{mark} {r['title']}\")\n",
    "            if r[\"message\"]:\n",
    "                print(f\"    {r['message']}\")\n",
    "        passed = sum(r[\"status\"] == \"passed\" for r in results)\n",
    "        print(\"─\" * 40)\n",
    "        print(f\"Прошло {passed} из {len(results)}\" + (\" — всё верно!\" if passed == len(results) else \"\"))\n",
    "        if hung:\n",
    "            print(\"Выполнение остановлено: один из тестов завис.\")\n",
    "            sys.stdout.flush()\n",
    "            os._exit(1)\n",
    "    return results\n",
    "\n",
    "\n",
    "def _check(*tests, names=()):\n",
    "    \"\"\"Запускает проверки упражнения и печатает ✓ / ✗ и итог — как на сайте.\"\"\"\n",
    "    for name in names:\n",
    "        if \".\" in name:  # «Класс.метод» из заготовки: класс должен быть объявлен\n",
    "            if not isinstance(globals().get(name.split(\".\")[0]), type):\n",
    "                print(f\"✗ класс {name.split('.')[0]} не объявлен — выполните ячейку упражнения\")\n",
    "                return\n",
    "            continue\n",
    "        if name not in globals():\n",
    "            print(f\"✗ переменная {name} не создана — выполните ячейку упражнения\")\n",
    "            return\n",
    "        if globals()[name] is Ellipsis:\n",
    "            print(f\"✗ {name} пока равна ... — замените многоточие своим кодом\")\n",
    "            return\n",
    "    globals()[\"_TESTS\"] = list(tests)\n",
    "    _run_tests()"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "setup"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "«Кофейная лавка» печатает новые ценники к осенней распродаже. На каждом — название, объём и цена, а у\n",
    "напитков со скидкой — цена после скидки. Заодно владелец хочет знать, какой напиток выгоднее всего по цене\n",
    "за 100 мл. Почти всё в проекте — из модуля: класс, `__init__`, методы и экземпляры в списке. Новое только одно —\n",
    "в шаге 2 метод вызовет другой метод того же экземпляра.\n",
    "\n",
    "## Шаг 1. Класс товара"
   ],
   "id": "text-10"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 1. Класс Product\n",
    "\n",
    "Допишите класс `Product`. `Product(\"Капучино\", 220, 300)` хранит название, цену в рублях и объём в миллилитрах\n",
    "в атрибутах `name`, `price` и `volume`. Методы:\n",
    "\n",
    "- `per_100ml()` возвращает цену за 100 мл, округлённую до одного знака после запятой: у капучино — 73.3;\n",
    "- `discounted(percent)` возвращает цену со скидкой `percent` процентов, округлённую до целых рублей, и **не\n",
    "  меняет** `price`: `discounted(20)` у капучино — 176.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Цена за 100 мл — `round(self.price / self.volume * 100, 1)`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Цена со скидкой — `round(self.price * (100 - percent) / 100)`; в `self.price` ничего не записывайте.\n",
    "\n",
    "</details>"
   ],
   "id": "product-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "class Product:\n",
    "    def __init__(self, name, price, volume):\n",
    "        ...\n",
    "\n",
    "    def per_100ml(self):\n",
    "        ...\n",
    "\n",
    "    def discounted(self, percent):\n",
    "        ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "product"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 1 — выполните после своего решения\n",
    "def _cappuccino():\n",
    "    try:\n",
    "        return Product(\"Капучино\", 220, 300)\n",
    "    except TypeError as e:\n",
    "        assert False, f\"Product(\\\"Капучино\\\", 220, 300) не создаётся: {e}\"\n",
    "\n",
    "\n",
    "def test_attributes():\n",
    "    \"name, price и volume из аргументов\"\n",
    "    p = _cappuccino()\n",
    "    for attr, value in [(\"name\", \"Капучино\"), (\"price\", 220), (\"volume\", 300)]:\n",
    "        assert hasattr(p, attr), f\"у товара нет атрибута {attr} — запишите его в __init__: self.{attr} = {attr}\"\n",
    "        assert getattr(p, attr) == value, f\"у Product(\\\"Капучино\\\", 220, 300) {attr} = {getattr(p, attr)!r}, а нужно {value!r}\"\n",
    "\n",
    "\n",
    "def test_per_100ml():\n",
    "    \"per_100ml() — цена за 100 мл с одним знаком\"\n",
    "    p = _cappuccino()\n",
    "    assert hasattr(p, \"price\") and hasattr(p, \"volume\"), \"сначала задайте атрибуты в __init__\"\n",
    "    got = p.per_100ml()\n",
    "    assert got is not None, \"per_100ml() ничего не возвращает — нужен return\"\n",
    "    assert got != round(300 / 220 * 100, 1), \"дробь перевёрнута: цена за 100 мл — цена, делённая на объём, умноженная на 100\"\n",
    "    assert got == 73.3, f\"per_100ml() у капучино = {got!r}, а нужно 73.3 — округлите до одного знака: round(…, 1)\"\n",
    "    assert Product(\"Латте\", 240, 400).per_100ml() == 60.0, \"у Product(\\\"Латте\\\", 240, 400) цена за 100 мл — 60.0\"\n",
    "\n",
    "\n",
    "def test_discounted():\n",
    "    \"discounted(20) = 176 и price не меняется\"\n",
    "    p = _cappuccino()\n",
    "    assert hasattr(p, \"price\"), \"сначала задайте атрибуты в __init__\"\n",
    "    got = p.discounted(20)\n",
    "    assert got is not None, \"discounted() ничего не возвращает — нужен return\"\n",
    "    assert p.price == 220, f\"после discounted(20) price = {p.price!r}: метод должен вернуть новую цену, а не менять атрибут\"\n",
    "    assert got != 44, \"44 — это размер скидки, а нужна цена после скидки\"\n",
    "    assert got == 176, f\"discounted(20) у капучино = {got!r}, а нужно 176\"\n",
    "    assert Product(\"Чай\", 133, 400).discounted(10) == 120, \"Product(\\\"Чай\\\", 133, 400).discounted(10) — 120 (119.7 после скидки): округлите до целых рублей\"\n",
    "\n",
    "_check(test_attributes, test_per_100ml, test_discounted, names=[\"Product.__init__\",\"Product.per_100ml\",\"Product.discounted\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "product-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "class Product:\n",
    "    def __init__(self, name, price, volume):\n",
    "        self.name = name\n",
    "        self.price = price\n",
    "        self.volume = volume\n",
    "\n",
    "    def per_100ml(self):\n",
    "        return round(self.price / self.volume * 100, 1)\n",
    "\n",
    "    def discounted(self, percent):\n",
    "        return round(self.price * (100 - percent) / 100)\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "product-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Шаг 2. Ценник\n",
    "\n",
    "Метод может вызывать другие методы того же экземпляра — через `self`: `self.discounted(percent)`."
   ],
   "id": "text-29"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 2. Строка ценника\n",
    "\n",
    "Добавьте в класс метод `card(percent=0)` — текст ценника. Без скидки: `\"Капучино, 300 мл — 220 ₽\"`, со\n",
    "скидкой — цена после скидки и пометка: `\"Капучино, 300 мл — 176 ₽ (скидка 20 %)\"`.\n",
    "\n",
    "Класс объявляется одним блоком, поэтому скопируйте в ячейку класс из шага 1 целиком и допишите новый метод.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Если `percent == 0` — верните строку с `self.price`, иначе — с `self.discounted(percent)` и пометкой.\n",
    "\n",
    "</details>"
   ],
   "id": "card-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "class Product:\n",
    "    # скопируйте сюда __init__, per_100ml и discounted из шага 1\n",
    "\n",
    "    def card(self, percent=0):\n",
    "        ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "card"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 2 — выполните после своего решения\n",
    "def _cappuccino():\n",
    "    try:\n",
    "        return Product(\"Капучино\", 220, 300)\n",
    "    except TypeError:\n",
    "        assert False, \"Product(\\\"Капучино\\\", 220, 300) не создаётся — скопируйте в класс __init__ из шага 1\"\n",
    "\n",
    "\n",
    "def test_copied():\n",
    "    \"методы шага 1 скопированы и работают\"\n",
    "    p = _cappuccino()\n",
    "    for method in [\"per_100ml\", \"discounted\"]:\n",
    "        assert hasattr(p, method), f\"в классе нет метода {method} — скопируйте класс из шага 1 целиком\"\n",
    "    assert p.per_100ml() == 73.3, f\"per_100ml() у капучино вернул {p.per_100ml()!r}, а нужно 73.3 — в скопированном классе ошибка из шага 1\"\n",
    "    assert p.discounted(20) == 176, f\"discounted(20) у капучино вернул {p.discounted(20)!r}, а нужно 176 — в скопированном классе ошибка из шага 1\"\n",
    "\n",
    "\n",
    "def test_plain():\n",
    "    \"card() без скидки\"\n",
    "    p = _cappuccino()\n",
    "    got = p.card()\n",
    "    assert got is not None, \"card() ничего не возвращает — нужен return\"\n",
    "    assert got == \"Капучино, 300 мл — 220 ₽\", f\"card() вернул {got!r}, а нужно \\\"Капучино, 300 мл — 220 ₽\\\"\"\n",
    "\n",
    "\n",
    "def test_sale():\n",
    "    \"card(20) — цена со скидкой и пометка\"\n",
    "    p = _cappuccino()\n",
    "    assert hasattr(p, \"discounted\"), \"сначала верните в класс метод discounted\"\n",
    "    got = p.card(20)\n",
    "    assert got != \"Капучино, 300 мл — 220 ₽ (скидка 20 %)\", \"на ценнике старая цена — возьмите цену со скидкой: self.discounted(percent)\"\n",
    "    assert got == \"Капучино, 300 мл — 176 ₽ (скидка 20 %)\", f\"card(20) вернул {got!r}, а нужно \\\"Капучино, 300 мл — 176 ₽ (скидка 20 %)\\\"\"\n",
    "    assert p.price == 220, \"card() не должен менять price\"\n",
    "\n",
    "_check(test_copied, test_plain, test_sale, names=[\"Product.card\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "card-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "class Product:\n",
    "    def __init__(self, name, price, volume):\n",
    "        self.name = name\n",
    "        self.price = price\n",
    "        self.volume = volume\n",
    "\n",
    "    def per_100ml(self):\n",
    "        return round(self.price / self.volume * 100, 1)\n",
    "\n",
    "    def discounted(self, percent):\n",
    "        return round(self.price * (100 - percent) / 100)\n",
    "\n",
    "    def card(self, percent=0):\n",
    "        if percent == 0:\n",
    "            return f\"{self.name}, {self.volume} мл — {self.price} ₽\"\n",
    "        return f\"{self.name}, {self.volume} мл — {self.discounted(percent)} ₽ (скидка {percent} %)\"\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "card-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> **Старые экземпляры — от старого класса.** Когда ячейка с классом выполняется заново, Python создаёт **новый** класс с тем же именем. Экземпляры,\n",
    "> созданные раньше, остаются экземплярами прежнего класса — без новых методов. Поэтому после изменения класса\n",
    "> выполните заново и ячейки, где создаются экземпляры."
   ],
   "id": "component-43"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Шаг 3. Каталог"
   ],
   "id": "text-48"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 3. Каталог напитков\n",
    "\n",
    "В `data` — тройки «название, цена, объём». Создайте список `catalog` из экземпляров `Product` (из шага 2)\n",
    "по этим данным, в том же порядке.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "`[Product(name, price, volume) for name, price, volume in data]`.\n",
    "\n",
    "</details>"
   ],
   "id": "catalog-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "data = [\n",
    "    (\"Эспрессо\", 150, 60),\n",
    "    (\"Капучино\", 220, 300),\n",
    "    (\"Латте\", 240, 400),\n",
    "    (\"Раф\", 260, 300),\n",
    "    (\"Американо\", 170, 250),\n",
    "    (\"Какао\", 190, 300),\n",
    "]\n",
    "catalog = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "catalog"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 3 — выполните после своего решения\n",
    "def test_catalog():\n",
    "    \"catalog — шесть экземпляров Product по порядку\"\n",
    "    assert isinstance(catalog, list), f\"catalog — это {type(catalog).__name__}, а нужен список\"\n",
    "    assert len(catalog) == 6, f\"в catalog {len(catalog)} элементов, а в data шесть троек\"\n",
    "    assert all(isinstance(p, Product) for p in catalog), \"в catalog должны быть экземпляры текущего класса Product — создайте их заново после ячейки с классом\"\n",
    "    assert [p.name for p in catalog] == [d[0] for d in data], f\"названия в catalog: {[p.name for p in catalog]}\"\n",
    "    assert [(p.price, p.volume) for p in catalog] == [d[1:] for d in data], \"цены и объёмы берутся из data: Product(name, price, volume)\"\n",
    "\n",
    "_check(test_catalog, names=[\"catalog\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "catalog-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "data = [\n",
    "    (\"Эспрессо\", 150, 60),\n",
    "    (\"Капучино\", 220, 300),\n",
    "    (\"Латте\", 240, 400),\n",
    "    (\"Раф\", 260, 300),\n",
    "    (\"Американо\", 170, 250),\n",
    "    (\"Какао\", 190, 300),\n",
    "]\n",
    "catalog = [Product(name, price, volume) for name, price, volume in data]\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "catalog-solution"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Каталог по строкам\n",
    "for p in catalog:\n",
    "    print(p.card())"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "look"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Шаг 4. Ценники к распродаже"
   ],
   "id": "text-59"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 4. Все ценники со скидкой\n",
    "\n",
    "На распродаже все напитки — со скидкой 20 %. Сохраните в `cards` список текстов ценников для всего каталога\n",
    "по порядку, а в `sale_total` — сколько стоит взять по одному напитку каждого вида по ценам распродажи.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "`catalog` уже есть: `[p.card(20) for p in catalog]`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Сумма цен со скидкой — `sum(p.discounted(20) for p in catalog)`: `sum` можно передать выражение «как генератор списка», но без квадратных скобок — это то же, что `sum([...])`.\n",
    "\n",
    "</details>"
   ],
   "id": "cards-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "cards = ...\n",
    "sale_total = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "cards"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 4 — выполните после своего решения\n",
    "def test_cards():\n",
    "    \"cards — шесть ценников со скидкой 20 %\"\n",
    "    assert isinstance(cards, list), f\"cards — это {type(cards).__name__}, а нужен список\"\n",
    "    assert len(cards) == 6, f\"в cards {len(cards)} элементов, а товаров в каталоге 6\"\n",
    "    assert all(isinstance(c, str) for c in cards), \"в cards должны быть строки — результаты вызова p.card(20) со скобками\"\n",
    "    assert cards[1] != \"Капучино, 300 мл — 220 ₽\", \"ценники без скидки — передайте в card процент: p.card(20)\"\n",
    "    assert cards[1] == \"Капучино, 300 мл — 176 ₽ (скидка 20 %)\", f\"второй ценник: {cards[1]!r}\"\n",
    "\n",
    "\n",
    "def test_total():\n",
    "    \"sale_total — сумма цен распродажи\"\n",
    "    assert sale_total != 1230, \"1230 — сумма цен без скидки; сложите p.discounted(20)\"\n",
    "    assert sale_total == 984, f\"sale_total = {sale_total!r}, а по ценам распродажи — 984 ₽\"\n",
    "\n",
    "_check(test_cards, test_total, names=[\"cards\",\"sale_total\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "cards-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "cards = [p.card(20) for p in catalog]\n",
    "sale_total = sum(p.discounted(20) for p in catalog)\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "cards-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Шаг 5. Самый выгодный"
   ],
   "id": "text-69"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 5. Выгоднее всего\n",
    "\n",
    "Найдите в каталоге напиток с самой низкой ценой за 100 мл и сохраните **сам экземпляр** в `best`.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Возьмите первый товар как лучший и пройдите циклом по каталогу: если у товара `per_100ml()` меньше, чем у лучшего, он становится лучшим.\n",
    "\n",
    "</details>"
   ],
   "id": "best-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "best = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "best"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 5 — выполните после своего решения\n",
    "def test_best():\n",
    "    \"best — товар с самой низкой ценой за 100 мл\"\n",
    "    assert not isinstance(best, (int, float)), f\"best = {best!r} — это число, а нужен сам товар, экземпляр Product\"\n",
    "    assert not isinstance(best, str), f\"best = {best!r} — это строка, а нужен сам товар, экземпляр Product\"\n",
    "    assert isinstance(best, Product), f\"best — это {type(best).__name__}, а нужен экземпляр Product из catalog\"\n",
    "    assert best.name != \"Эспрессо\", \"эспрессо — самый дешёвый за чашку, но самый дорогой за 100 мл: сравнивайте per_100ml()\"\n",
    "    assert best.name == \"Латте\", f\"best — {best.name}, а дешевле всех за 100 мл латте\"\n",
    "\n",
    "_check(test_best, names=[\"best\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "best-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "best = catalog[0]\n",
    "for p in catalog:\n",
    "    if p.per_100ml() < best.per_100ml():\n",
    "        best = p\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "best-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Итог"
   ],
   "id": "text-77"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Ценники распродажи\n",
    "print(\"Осенняя распродажа — скидка 20 %\")\n",
    "for text in cards:\n",
    "    print(\" \", text)\n",
    "print(f\"Всё меню по одной чашке: {sale_total} ₽\")\n",
    "print(f\"Выгоднее всего: {best.name} — {best.per_100ml()} ₽ за 100 мл\")"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "summary"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*После решения всех упражнений урока:*\n",
    "\n",
    "Самый выгодный напиток — латте: 60 ₽ за 100 мл, вчетверо дешевле эспрессо, хотя эспрессо в меню самый дешёвый.\n",
    "Цена за чашку и цена за объём — разные вопросы, и класс `Product` отвечает на оба одним вызовом метода."
   ],
   "id": "component-82"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрите, что дал класс: данные товара и всё, что с ним считают, лежат в одном месте. Чтобы поменять формат\n",
    "ценника, достаточно поправить один метод `card` — и он изменится у всех товаров сразу. В следующем модуле\n",
    "экземпляры начнут хранить другие экземпляры: появится корзина с товарами."
   ],
   "id": "text-86"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "colab": {
   "provenance": []
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
