{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Абстрактные классы\n",
    "\n",
    "Абстрактный класс — наследник ABC с методами @abstractmethod: создать его нельзя, а подкласс обязан эти методы реализовать. TypeError при создании и шаблонный метод.\n",
    "\n",
    "Курс ООП в Python · урок `oop-abc` · [урок на сайте](https://ds-edu-trainer.vercel.app/courses/oop/abc).\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": [
    "В проекте про скидки базовый класс `Discount` был «заглушкой»: его метод `amount` возвращал 0, а подклассы\n",
    "его переопределяли. Но ничто не мешало создать сам `Discount` или подкласс, где `amount` забыли, — касса\n",
    "просто посчитала бы скидку 0, и ошибку никто бы не заметил. Нужен способ сказать: «этот класс — только\n",
    "основа для других, а такой-то метод подкласс **обязан** написать».\n",
    "\n",
    "## `ABC` и `@abstractmethod`\n",
    "\n",
    "Такой класс называется **абстрактным** (abstract base class, ABC). В Python его объявляют наследником\n",
    "класса `ABC` из модуля `abc` стандартной библиотеки, а обязательные методы помечают декоратором\n",
    "`@abstractmethod`; такой метод называют **абстрактным методом** (abstract method). Тело абстрактного метода\n",
    "обычно пустое — `...`: реализацию дадут подклассы. Декоратор работает только в наследнике `ABC`: в обычном\n",
    "классе `@abstractmethod` молча ничего не запрещает."
   ],
   "id": "text-9"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Абстрактный способ доставки\n",
    "from abc import ABC, abstractmethod\n",
    "\n",
    "\n",
    "class Delivery(ABC):\n",
    "    def __init__(self, title):\n",
    "        self.title = title\n",
    "\n",
    "    @abstractmethod\n",
    "    def cost(self, km):\n",
    "        ...  # реализацию дадут подклассы\n",
    "\n",
    "    def describe(self, km):  # обычный метод — пользуется абстрактным\n",
    "        return f\"{self.title}: {self.cost(km)} ₽\"\n",
    "\n",
    "\n",
    "print(Delivery.__name__, issubclass(Delivery, ABC))"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "abc"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Создать экземпляр абстрактного класса нельзя — `TypeError` сразу при создании, а не где-то потом:"
   ],
   "id": "text-24"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Абстрактный класс не создаётся\n",
    "# Эта ячейка показывает ошибку TypeError: она перехвачена, чтобы «Выполнить всё» не останавливалось.\n",
    "try:\n",
    "    Delivery(\"какая-то доставка\")\n",
    "except Exception as error:\n",
    "    print(f\"{type(error).__name__}: {error}\")"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "no-instance"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "То же с подклассом, который реализовал не все абстрактные методы. А подкласс, где все они есть, — обычный\n",
    "класс:"
   ],
   "id": "text-28"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Подкласс без метода и с методом\n",
    "class Pickup(Delivery):\n",
    "    def cost(self, km):\n",
    "        return 0\n",
    "\n",
    "\n",
    "class BrokenCourier(Delivery):\n",
    "    def cots(self, km):  # опечатка: cost так и не реализован\n",
    "        return 150 + 30 * km\n",
    "\n",
    "\n",
    "print(Pickup(\"самовывоз\").describe(5))\n",
    "try:\n",
    "    BrokenCourier(\"курьер\")\n",
    "except TypeError as error:\n",
    "    print(\"TypeError:\", error)"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "subclasses"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> **Типичная ошибка: TypeError при создании экземпляра подкласса.**\n",
    ">\n",
    "> Ошибка вида `Can't instantiate abstract class … without an implementation for abstract method 'cost'` (так она\n",
    "> звучит в Python 3.12 и новее; в 3.10–3.11 — `… with abstract method cost`) при создании экземпляра значит: в подклассе нет одного из абстрактных методов — его имя есть в\n",
    "> конце сообщения. Частая причина — опечатка в имени метода: `def cots(self, km)` не реализует `cost`."
   ],
   "id": "component-34"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 1. Абстрактный напиток\n",
    "\n",
    "Объявите абстрактный класс `Beverage`: `__init__(self, name)` записывает название в `name`, абстрактный метод\n",
    "`ingredients()` должен вернуть список ингредиентов, а обычный метод `card()` возвращает строку вида\n",
    "`\"Латте: эспрессо, молоко\"` — название и ингредиенты через запятую.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "`class Beverage(ABC):`, над методом `ingredients` — `@abstractmethod`, тело — `...`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "`card` вызывает `self.ingredients()` — подкласс подставит свой список, а соединить его поможет `\", \".join(...)`.\n",
    "\n",
    "</details>"
   ],
   "id": "beverage-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "from abc import ABC, abstractmethod\n",
    "\n",
    "\n",
    "class Beverage:  # сделайте класс абстрактным\n",
    "    def __init__(self, name):\n",
    "        ...\n",
    "\n",
    "    def ingredients(self):  # сделайте метод абстрактным\n",
    "        ...\n",
    "\n",
    "    def card(self):\n",
    "        ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "beverage"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 1 — выполните после своего решения\n",
    "def _cannot_create(cls, *args):\n",
    "    try:\n",
    "        cls(*args)\n",
    "    except TypeError:\n",
    "        return True\n",
    "    return False\n",
    "\n",
    "\n",
    "def test_abstract():\n",
    "    \"Beverage — абстрактный класс\"\n",
    "    assert issubclass(Beverage, ABC), \"Beverage должен наследовать от ABC: class Beverage(ABC):\"\n",
    "    assert \"ingredients\" in getattr(Beverage, \"__abstractmethods__\", set()), \"ingredients должен быть абстрактным — поставьте над ним @abstractmethod\"\n",
    "    assert _cannot_create(Beverage, \"Латте\"), \"экземпляр Beverage создаётся, а у абстрактного класса не должен\"\n",
    "\n",
    "\n",
    "def test_card():\n",
    "    \"card собирает строку из ingredients подкласса\"\n",
    "    assert issubclass(Beverage, ABC), \"сначала сделайте Beverage абстрактным\"\n",
    "\n",
    "    class Tea(Beverage):\n",
    "        def ingredients(self):\n",
    "            return [\"чай\", \"вода\"]\n",
    "    tea = Tea(\"Чай\")\n",
    "    assert getattr(tea, \"name\", None) == \"Чай\", \"название хранится в атрибуте name\"\n",
    "    got = tea.card()\n",
    "    assert got == \"Чай: чай, вода\", f\"card() вернул {got!r}, а для ингредиентов [\\\"чай\\\", \\\"вода\\\"] нужно \\\"Чай: чай, вода\\\"\"\n",
    "\n",
    "_check(test_abstract, test_card, names=[\"Beverage.__init__\",\"Beverage.ingredients\",\"Beverage.card\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "beverage-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "from abc import ABC, abstractmethod\n",
    "\n",
    "\n",
    "class Beverage(ABC):\n",
    "    def __init__(self, name):\n",
    "        self.name = name\n",
    "\n",
    "    @abstractmethod\n",
    "    def ingredients(self):\n",
    "        ...\n",
    "\n",
    "    def card(self):\n",
    "        return f\"{self.name}: {', '.join(self.ingredients())}\"\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "beverage-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 2. Эспрессо и латте\n",
    "\n",
    "Объявите два подкласса `Beverage`: `Espresso` — ингредиенты `[\"эспрессо\"]`, `Latte` — `[\"эспрессо\", \"молоко\"]`.\n",
    "Название передаётся при создании: `Latte(\"Латте\")`.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Каждому подклассу достаточно одного метода `ingredients`; `__init__` и `card` наследуются.\n",
    "\n",
    "</details>"
   ],
   "id": "drinks-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "class Espresso(Beverage):\n",
    "    def ingredients(self):\n",
    "        ...\n",
    "\n",
    "\n",
    "class Latte(Beverage):\n",
    "    def ingredients(self):\n",
    "        ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "drinks"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 2 — выполните после своего решения\n",
    "def _make(cls, name):\n",
    "    try:\n",
    "        return cls(name)\n",
    "    except TypeError as e:\n",
    "        assert False, f\"{cls.__name__}(\\\"{name}\\\") не создаётся: {e}\"\n",
    "\n",
    "\n",
    "def test_espresso():\n",
    "    \"Espresso — один ингредиент\"\n",
    "    assert issubclass(Espresso, Beverage), \"Espresso должен наследовать от Beverage\"\n",
    "    got = _make(Espresso, \"Эспрессо\").card()\n",
    "    assert got == \"Эспрессо: эспрессо\", f\"card() эспрессо = {got!r}\"\n",
    "\n",
    "\n",
    "def test_latte():\n",
    "    \"Latte — эспрессо и молоко\"\n",
    "    assert issubclass(Latte, Beverage), \"Latte должен наследовать от Beverage\"\n",
    "    latte = _make(Latte, \"Латте\")\n",
    "    assert latte.ingredients() == [\"эспрессо\", \"молоко\"], f\"ingredients() латте = {latte.ingredients()!r}\"\n",
    "    assert latte.card() == \"Латте: эспрессо, молоко\", f\"card() латте = {latte.card()!r}\"\n",
    "\n",
    "_check(test_espresso, test_latte, names=[\"Espresso.ingredients\",\"Latte.ingredients\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "drinks-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "class Espresso(Beverage):\n",
    "    def ingredients(self):\n",
    "        return [\"эспрессо\"]\n",
    "\n",
    "\n",
    "class Latte(Beverage):\n",
    "    def ingredients(self):\n",
    "        return [\"эспрессо\", \"молоко\"]\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "drinks-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Шаблонный метод\n",
    "\n",
    "В `card()` вы написали обычный метод абстрактного класса, который вызывает абстрактный. Это частый приём —\n",
    "**шаблонный метод** (template method): базовый класс задаёт порядок действий целиком, а подклассы\n",
    "заполняют отдельные шаги. Порядок не повторяется в каждом подклассе, и по договорённости подклассы шаблонный\n",
    "метод не переопределяют — хотя Python этого не запрещает."
   ],
   "id": "text-55"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Отчёт: шаблон в базовом классе\n",
    "class Report(ABC):\n",
    "    def render(self):  # шаблон: порядок задан здесь\n",
    "        lines = [self.title(), \"-\" * len(self.title())]\n",
    "        lines += self.lines()\n",
    "        return \"\\n\".join(lines)\n",
    "\n",
    "    @abstractmethod\n",
    "    def title(self):\n",
    "        ...\n",
    "\n",
    "    @abstractmethod\n",
    "    def lines(self):\n",
    "        ...\n",
    "\n",
    "\n",
    "class StockReport(Report):\n",
    "    def __init__(self, stock):\n",
    "        self.stock = stock\n",
    "\n",
    "    def title(self):\n",
    "        return \"Остатки\"\n",
    "\n",
    "    def lines(self):\n",
    "        return [f\"{name} — {left} шт.\" for name, left in self.stock.items()]\n",
    "\n",
    "\n",
    "print(StockReport({\"зёрна\": 12, \"молоко\": 4}).render())"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "template"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 3. Отчёт о продажах\n",
    "\n",
    "Объявите подкласс `SalesReport` класса `Report` из демонстрации. `SalesReport({\"Латте\": 41, \"Раф\": 17})`\n",
    "хранит словарь продаж в атрибуте `sales`. Заголовок отчёта — `\"Продажи за день\"`, строки — `\"Латте: 41\"`,\n",
    "`\"Раф: 17\"` и так далее в порядке словаря. Метод `render` не переопределяйте.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Свой `__init__` не обязан вызывать `super().__init__`, если у базового класса его нет.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "`lines()` возвращает список строк — по одной на пару «название, количество» из словаря `self.sales`.\n",
    "\n",
    "</details>"
   ],
   "id": "sales-report-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "class SalesReport(Report):\n",
    "    def __init__(self, sales):\n",
    "        ...\n",
    "\n",
    "    def title(self):\n",
    "        ...\n",
    "\n",
    "    def lines(self):\n",
    "        ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "sales-report"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 3 — выполните после своего решения\n",
    "def _report():\n",
    "    try:\n",
    "        return SalesReport({\"Латте\": 41, \"Раф\": 17})\n",
    "    except TypeError as e:\n",
    "        assert False, f\"SalesReport(...) не создаётся: {e}\"\n",
    "\n",
    "\n",
    "def test_parts():\n",
    "    \"title и lines\"\n",
    "    assert issubclass(SalesReport, Report), \"SalesReport должен наследовать от Report\"\n",
    "    report = _report()\n",
    "    assert getattr(report, \"sales\", None) == {\"Латте\": 41, \"Раф\": 17}, \"словарь продаж хранится в атрибуте sales\"\n",
    "    assert report.title() == \"Продажи за день\", f\"title() = {report.title()!r}\"\n",
    "    assert report.lines() == [\"Латте: 41\", \"Раф: 17\"], f\"lines() = {report.lines()!r}\"\n",
    "\n",
    "\n",
    "def test_render():\n",
    "    \"render из базового класса\"\n",
    "    assert \"render\" not in SalesReport.__dict__, \"render переопределять не нужно — он уже есть в Report\"\n",
    "    got = _report().render()\n",
    "    assert got == \"Продажи за день\\n---------------\\nЛатте: 41\\nРаф: 17\", f\"render() дал:\\n{got}\"\n",
    "\n",
    "_check(test_parts, test_render, names=[\"SalesReport.__init__\",\"SalesReport.title\",\"SalesReport.lines\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "sales-report-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "class SalesReport(Report):\n",
    "    def __init__(self, sales):\n",
    "        self.sales = sales\n",
    "\n",
    "    def title(self):\n",
    "        return \"Продажи за день\"\n",
    "\n",
    "    def lines(self):\n",
    "        return [f\"{name}: {count}\" for name, count in self.sales.items()]\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "sales-report-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Вопрос.** У абстрактного класса `Report` два абстрактных метода: `title` и `lines`. Подкласс реализовал только\n",
    "`lines`. Когда появится ошибка?\n",
    "\n",
    "- при объявлении подкласса\n",
    "- при создании экземпляра подкласса\n",
    "- при вызове `render()`\n",
    "\n",
    "<details><summary>Ответ</summary>\n",
    "\n",
    "при создании экземпляра подкласса\n",
    "\n",
    "Объявить такой подкласс можно — он сам остаётся абстрактным. А при попытке создать экземпляр Python проверит, что абстрактных методов не осталось, и выбросит `TypeError`.\n",
    "\n",
    "</details>"
   ],
   "id": "quiz-74"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Абстрактный класс — это договор: «каждый способ доставки умеет `cost`», «каждый отчёт умеет `title` и\n",
    "`lines`». Код, который пользуется такими объектами, может на этот договор полагаться."
   ],
   "id": "text-84"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "colab": {
   "provenance": []
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
