{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Атрибуты класса и экземпляра\n",
    "\n",
    "Атрибут класса общий для всех экземпляров, атрибут экземпляра — у каждого свой. Константы класса, порядок поиска атрибута и список в атрибуте класса, который делят все.\n",
    "\n",
    "Курс ООП в Python · урок `oop-class-attrs` · [урок на сайте](https://ds-edu-trainer.vercel.app/courses/oop/class-attrs).\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": [
    "До сих пор все атрибуты записывались в `__init__` через `self` — у каждого экземпляра свои. Но бывают данные,\n",
    "общие для всех экземпляров класса: валюта цен, предельная скидка, счётчик созданных заказов. Их хранят\n",
    "в самом классе.\n",
    "\n",
    "## Атрибут класса\n",
    "\n",
    "Переменная, объявленная прямо в теле класса (не внутри метода), — **атрибут класса** (class attribute).\n",
    "Он один на весь класс, и его видно и через класс, и через любой экземпляр. Постоянные значения — константы\n",
    "класса — называют заглавными буквами:"
   ],
   "id": "text-9"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Валюта — одна на всех\n",
    "class Product:\n",
    "    CURRENCY = \"₽\"  # атрибут класса: один на всех\n",
    "\n",
    "    def __init__(self, name, price):\n",
    "        self.name = name\n",
    "        self.price = price\n",
    "\n",
    "    def label(self):\n",
    "        return f\"{self.name} — {self.price} {self.CURRENCY}\"\n",
    "\n",
    "\n",
    "latte = Product(\"Латте\", 220)\n",
    "tea = Product(\"Чай\", 120)\n",
    "print(Product.CURRENCY)\n",
    "print(latte.CURRENCY, tea.CURRENCY)\n",
    "print(latte.label())"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "class-attr"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Где Python ищет атрибут\n",
    "\n",
    "Атрибуты экземпляра хранятся в словаре `__dict__` самого экземпляра, атрибуты класса — в `__dict__` класса.\n",
    "Когда вы пишете `latte.CURRENCY`, Python сначала ищет имя у экземпляра, а не найдя — у его класса (так\n",
    "ищутся обычные атрибуты; у свойств, о которых модуль 3, свои правила):"
   ],
   "id": "text-21"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Словари экземпляра и класса\n",
    "print(latte.__dict__)\n",
    "print(\"CURRENCY\" in latte.__dict__)\n",
    "print(\"CURRENCY\" in Product.__dict__)"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "lookup"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Поэтому метод может читать константу как `self.CURRENCY`: у экземпляра её нет, и Python найдёт её в классе."
   ],
   "id": "text-29"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 1. Предельная скидка\n",
    "\n",
    "Допишите класс `Product`. Добавьте атрибут класса `MAX_DISCOUNT = 30` — больше 30 % скидки кофейня не даёт.\n",
    "Метод `discounted(percent)` возвращает цену со скидкой, округлённую до целых, но если `percent` больше\n",
    "`MAX_DISCOUNT`, применяется `MAX_DISCOUNT`: `Product(\"Латте\", 200).discounted(50)` — 140.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Константа объявляется в теле класса, рядом с методами: `MAX_DISCOUNT = 30`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Скидка с учётом предела — меньшее из `percent` и `self.MAX_DISCOUNT`: встроенная `min`.\n",
    "\n",
    "</details>"
   ],
   "id": "max-discount-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "class Product:\n",
    "    CURRENCY = \"₽\"\n",
    "    # объявите здесь MAX_DISCOUNT\n",
    "\n",
    "    def __init__(self, name, price):\n",
    "        self.name = name\n",
    "        self.price = price\n",
    "\n",
    "    def discounted(self, percent):\n",
    "        ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "max-discount"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 1 — выполните после своего решения\n",
    "def test_constant():\n",
    "    \"MAX_DISCOUNT = 30 — атрибут класса\"\n",
    "    assert \"MAX_DISCOUNT\" in Product.__dict__, \"в классе нет атрибута MAX_DISCOUNT — объявите его в теле класса, а не в __init__\"\n",
    "    assert Product.MAX_DISCOUNT == 30, f\"Product.MAX_DISCOUNT = {Product.MAX_DISCOUNT!r}, а нужно 30\"\n",
    "\n",
    "\n",
    "def test_small():\n",
    "    \"скидка до предела применяется как есть\"\n",
    "    got = Product(\"Латте\", 200).discounted(10)\n",
    "    assert got is not None, \"discounted() ничего не возвращает — нужен return\"\n",
    "    assert got == 180, f\"Product(\\\"Латте\\\", 200).discounted(10) = {got!r}, а нужно 180\"\n",
    "\n",
    "\n",
    "def test_capped():\n",
    "    \"больше 30 % — только 30 %\"\n",
    "    got = Product(\"Латте\", 200).discounted(50)\n",
    "    assert got != 100, \"скидка 50 % применилась целиком, а больше MAX_DISCOUNT давать нельзя\"\n",
    "    assert got == 140, f\"Product(\\\"Латте\\\", 200).discounted(50) = {got!r}, а нужно 140 — скидка не больше 30 %\"\n",
    "\n",
    "\n",
    "def test_uses_constant():\n",
    "    \"метод берёт предел из атрибута класса\"\n",
    "    assert \"MAX_DISCOUNT\" in Product.__dict__, \"сначала объявите MAX_DISCOUNT в классе\"\n",
    "    old = Product.MAX_DISCOUNT\n",
    "    try:\n",
    "        Product.MAX_DISCOUNT = 50\n",
    "        got = Product(\"Латте\", 200).discounted(60)\n",
    "    finally:\n",
    "        Product.MAX_DISCOUNT = old\n",
    "    assert got == 100, \"когда Product.MAX_DISCOUNT = 50, предел должен стать 50 % — читайте его из self.MAX_DISCOUNT, а не пишите 30 в методе\"\n",
    "\n",
    "_check(test_constant, test_small, test_capped, test_uses_constant, names=[\"Product.discounted\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "max-discount-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "class Product:\n",
    "    CURRENCY = \"₽\"\n",
    "    MAX_DISCOUNT = 30\n",
    "\n",
    "    def __init__(self, name, price):\n",
    "        self.name = name\n",
    "        self.price = price\n",
    "\n",
    "    def discounted(self, percent):\n",
    "        percent = min(percent, self.MAX_DISCOUNT)\n",
    "        return round(self.price * (100 - percent) / 100)\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "max-discount-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Запись атрибута через экземпляр\n",
    "\n",
    "Читать атрибут класса через экземпляр можно, а вот **запись** через экземпляр работает иначе:\n",
    "`latte.CURRENCY = \"$\"` создаёт атрибут *у экземпляра*, и он закрывает собой атрибут класса — только для этого\n",
    "экземпляра. Изменить общее значение можно только через класс:"
   ],
   "id": "text-40"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Запись через экземпляр и через класс\n",
    "latte = Product(\"Латте\", 220)\n",
    "tea = Product(\"Чай\", 120)\n",
    "latte.CURRENCY = \"$\"  # атрибут появился у экземпляра latte\n",
    "print(latte.CURRENCY, tea.CURRENCY, Product.CURRENCY)\n",
    "Product.CURRENCY = \"руб.\"  # меняем атрибут класса\n",
    "print(latte.CURRENCY, tea.CURRENCY, Product.CURRENCY)\n",
    "Product.CURRENCY = \"₽\"  # вернём как было"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "shadow"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Вопрос.** Что напечатает этот код?\n",
    "\n",
    "```python\n",
    "class Cup:\n",
    "    size = \"M\"\n",
    "\n",
    "a = Cup()\n",
    "b = Cup()\n",
    "a.size = \"L\"\n",
    "Cup.size = \"S\"\n",
    "print(a.size, b.size)\n",
    "```\n",
    "\n",
    "- `L L`\n",
    "- `L S`\n",
    "- `S S`\n",
    "\n",
    "<details><summary>Ответ</summary>\n",
    "\n",
    "`L S`\n",
    "\n",
    "`a.size = \"L\"` создал атрибут у экземпляра `a`, и он находится первым. У `b` своего `size` нет, поэтому `b.size` — это атрибут класса, а его изменили на `\"S\"`.\n",
    "\n",
    "</details>"
   ],
   "id": "quiz-49"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Из-за этого правила частая ошибка — счётчик в атрибуте класса, который увеличивают через `self`:"
   ],
   "id": "text-69"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# self.count += 1 не меняет счётчик класса\n",
    "class Ticket:\n",
    "    count = 0\n",
    "\n",
    "    def __init__(self):\n",
    "        self.count += 1  # ошибка: запись идёт в экземпляр\n",
    "\n",
    "\n",
    "first = Ticket()\n",
    "second = Ticket()\n",
    "print(Ticket.count, first.count, second.count)"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "counter-bug"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> **Типичная ошибка: self.count += 1 вместо Ticket.count += 1.**\n",
    ">\n",
    "> `self.count += 1` читает `count` из класса, прибавляет 1 и **записывает результат в экземпляр**. Атрибут\n",
    "> класса остаётся прежним, а у каждого талона появляется свой `count`, равный 1. Общее значение меняют через\n",
    "> имя класса: `Ticket.count += 1`."
   ],
   "id": "component-74"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 2. Номера заказов\n",
    "\n",
    "Объявите класс `Order`. Атрибут класса `created` — сколько заказов создано, вначале 0. Каждый новый заказ\n",
    "увеличивает `created` на 1 и получает атрибут экземпляра `number` — свой порядковый номер: первый заказ —\n",
    "номер 1, второй — 2 и так далее.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Счётчик класса меняют через имя класса: `Order.created`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Номер заказа — значение счётчика сразу после увеличения.\n",
    "\n",
    "</details>"
   ],
   "id": "numbering-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "class Order:\n",
    "    created = 0\n",
    "\n",
    "    def __init__(self):\n",
    "        ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "numbering"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 2 — выполните после своего решения\n",
    "def _fresh_orders(n):\n",
    "    \"n новых заказов при счётчике, сброшенном на 0; счётчик потом возвращается\"\n",
    "    saved = Order.created\n",
    "    try:\n",
    "        Order.created = 0\n",
    "        orders = [Order() for _ in range(n)]\n",
    "        return orders, Order.created\n",
    "    finally:\n",
    "        Order.created = saved\n",
    "\n",
    "\n",
    "def test_declared():\n",
    "    \"created — атрибут класса\"\n",
    "    assert \"created\" in Order.__dict__, \"в классе нет атрибута created — объявите в теле класса created = 0\"\n",
    "\n",
    "\n",
    "def test_counter():\n",
    "    \"каждый заказ увеличивает Order.created\"\n",
    "    assert \"created\" in Order.__dict__, \"сначала объявите created в классе\"\n",
    "    orders, created = _fresh_orders(3)\n",
    "    assert created != 0, \"после трёх заказов Order.created всё ещё 0: self.created += 1 меняет экземпляр, а не класс — пишите Order.created += 1\"\n",
    "    assert created == 3, f\"после трёх заказов Order.created = {created!r}, а нужно 3\"\n",
    "\n",
    "\n",
    "def test_numbers():\n",
    "    \"номера заказов 1, 2, 3\"\n",
    "    assert \"created\" in Order.__dict__, \"сначала объявите created в классе\"\n",
    "    orders, _ = _fresh_orders(3)\n",
    "    assert all(hasattr(o, \"number\") for o in orders), \"у заказа нет атрибута number — запишите его в __init__: self.number = …\"\n",
    "    numbers = [o.number for o in orders]\n",
    "    assert numbers == [1, 2, 3], f\"у трёх новых заказов номера {numbers}, а нужно [1, 2, 3]\"\n",
    "\n",
    "_check(test_declared, test_counter, test_numbers, names=[\"Order.__init__\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "numbering-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "class Order:\n",
    "    created = 0\n",
    "\n",
    "    def __init__(self):\n",
    "        Order.created += 1\n",
    "        self.number = Order.created\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "numbering-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Общий список — общая беда\n",
    "\n",
    "Атрибут класса со списком тоже один на всех. Если экземпляры добавляют в него элементы, добавляют они в\n",
    "**один и тот же** список:"
   ],
   "id": "text-88"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Две корзины с одним списком\n",
    "class Basket:\n",
    "    items = []  # ошибка: один список на все корзины\n",
    "\n",
    "    def add(self, name):\n",
    "        self.items.append(name)\n",
    "\n",
    "\n",
    "anna = Basket()\n",
    "boris = Basket()\n",
    "anna.add(\"Латте\")\n",
    "boris.add(\"Чай\")\n",
    "print(anna.items)\n",
    "print(boris.items)"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "shared-list"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> **Типичная ошибка: список в атрибуте класса общий для всех экземпляров.**\n",
    ">\n",
    "> `items = []` в теле класса создаётся один раз — когда объявляется класс. Все корзины видят этот единственный\n",
    "> список, и товар, положенный в одну, оказывается во всех. Список, который у каждого экземпляра свой, создают\n",
    "> в `__init__`: `self.items = []`."
   ],
   "id": "component-96"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 3. Починить столики\n",
    "\n",
    "В заготовке — класс `Table` (столик в зале) с той же ошибкой, что в демонстрации: заказы всех столиков\n",
    "попадают в один список. Исправьте класс так, чтобы у каждого столика был свой список `orders`, а метод\n",
    "`order(drink)` добавлял напиток только в заказы своего столика.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Список, который у каждого экземпляра свой, создаётся там же, где номер столика.\n",
    "\n",
    "</details>"
   ],
   "id": "fix-table-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "class Table:\n",
    "    orders = []\n",
    "\n",
    "    def __init__(self, number):\n",
    "        self.number = number\n",
    "\n",
    "    def order(self, drink):\n",
    "        self.orders.append(drink)"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "fix-table"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 3 — выполните после своего решения\n",
    "def _tables():\n",
    "    try:\n",
    "        return Table(1), Table(2)\n",
    "    except TypeError as e:\n",
    "        assert False, f\"Table(1) не создаётся: {e}. У __init__ параметры self и number\"\n",
    "\n",
    "\n",
    "def test_own_lists():\n",
    "    \"у двух столиков — свои заказы\"\n",
    "    a, b = _tables()\n",
    "    assert hasattr(a, \"orders\"), \"у столика нет атрибута orders — создайте список в __init__: self.orders = []\"\n",
    "    a.order(\"Латте\")\n",
    "    b.order(\"Чай\")\n",
    "    assert a.orders != [\"Латте\", \"Чай\"], \"заказы обоих столиков попали в один список — у каждого столика должен быть свой: self.orders = [] в __init__\"\n",
    "    assert a.orders == [\"Латте\"] and b.orders == [\"Чай\"], f\"у столиков заказы {a.orders} и {b.orders}, а нужно ['Латте'] и ['Чай']\"\n",
    "    assert a.number == 1 and b.number == 2, \"номер столика по-прежнему берётся из параметра number\"\n",
    "\n",
    "\n",
    "def test_new_empty():\n",
    "    \"у нового столика заказов нет\"\n",
    "    first, _ = _tables()\n",
    "    assert hasattr(first, \"orders\"), \"у столика нет атрибута orders — создайте список в __init__\"\n",
    "    first.order(\"Раф\")\n",
    "    fresh, _ = _tables()\n",
    "    assert fresh.orders == [], f\"у нового столика уже есть заказы {fresh.orders} — список не должен быть общим\"\n",
    "\n",
    "_check(test_own_lists, test_new_empty)"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "fix-table-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "class Table:\n",
    "    def __init__(self, number):\n",
    "        self.number = number\n",
    "        self.orders = []\n",
    "\n",
    "    def order(self, drink):\n",
    "        self.orders.append(drink)\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "fix-table-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Правило простое: атрибут класса — для того, что действительно общее и **не меняется** через экземпляры\n",
    "(константы, настройки). Всё, что у каждого объекта своё, — в `__init__` через `self`."
   ],
   "id": "text-109"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "colab": {
   "provenance": []
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
