# Домашнє завдання · Dataclass і сучасний стиль

> Лекція: [lecture.html](lecture.html) · Практика: [practice.ipynb](practice.ipynb) · Тест: [quiz.html](quiz.html)

Предметна область тут інша, ніж у лекції, — навмисно: якщо ти зможеш повторити ту саму
механіку на своїх даних, значить, справа не в завченому прикладі. Працюємо з
**карткою читача бібліотеки**. Усе роби в окремому файлі `chytachi.py` або в новому
зошиті — практику не редагуй, вона знадобиться для звірки.

---

## 🟢 Рівень 1 — База

Опиши картку читача через `@dataclass` і доведи, що фабрика справді створює окремий
список кожному.

1. Клас `Читач` із полями: `імʼя` (рядок), `рік_народження` (ціле), `квиток` (рядок),
   `взяті_книги` (список, порожній за замовчуванням).
2. **Спершу напиши `взяті_книги: list = []`** і збережи текст помилки в коментарі —
   слово в слово. Потім полагодь через `field(default_factory=list)`.
3. Створи двох читачів, додай книгу одному з них, надрукуй обох.
4. Надрукуй `list(Читач.__dataclass_fields__)` і `repr` одного читача.

**Зроблено, якщо:**

- у файлі є коментар із дослівним текстом `ValueError` про `mutable default` — і
  поруч робочий варіант із `default_factory`;
- проходить `assert перший.взяті_книги is not другий.взяті_книги`;
- проходить `assert другий.взяті_книги == []` після того, як першому додали книгу;
- проходить `assert list(Читач.__dataclass_fields__) == ["імʼя", "рік_народження",
  "квиток", "взяті_книги"]` — тобто порядок полів саме той, що ти оголосив.

---

## 🟡 Рівень 2 — Плюс

Додай до картки правила, яких декоратор згенерувати не може.

1. Напиши `__post_init__`, який кидає `ValueError`, якщо: імʼя порожнє або складається
   з пробілів; `рік_народження` поза межами 1900–2026.
2. Додай поле `вік: int = field(init=False)` і обчисли його в тому ж `__post_init__`
   (вважай поточним 2026 рік).
3. Додай службове поле `останній_вхід: float = field(default=0.0, compare=False)` — воно
   не має впливати на рівність двох карток.
4. Перевір, що поле `вік` **неможливо** передати в конструктор.

**Зроблено, якщо:**

- проходить `assert Читач("Олена", 1998, "Ч-17").вік == 28`;
- три `assert`-и через `try/except` ловлять `ValueError` для трьох поганих випадків
  (порожнє імʼя, рік 1800, рік 2100) — і кожен перевіряє **текст** повідомлення,
  а не сам факт винятку;
- проходить `assert Читач("Олена", 1998, "Ч-17", останній_вхід=9.0) ==
  Читач("Олена", 1998, "Ч-17", останній_вхід=21.0)` — тобто `compare=False` працює;
- проходить `assert "вік" not in inspect.signature(Читач).parameters`.

---

## 🔴 Рівень 3 — Виклик

Дослідь межу незмінності й доведи її числами, а не словами.

1. Зроби `ЗамороженийЧитач` через `@dataclass(frozen=True)` з тими самими полями,
   де `взяті_книги` — **список**. Доведи двома `assert`-ами: (а) поле не
   перепризначити, (б) список усередині **все одно** змінюється методом `append`.
2. Покажи, що такий обʼєкт не хешується, і що падає при цьому не `Читач`, а `list`:
   злови `TypeError` і перевір, що в тексті є слово `list`.
3. Зроби другу версію, де `взяті_книги` — кортеж. Доведи, що тепер `hash()` працює,
   а дві однакові картки в множині зливаються в одну.
4. Зміряй `tracemalloc`-ом памʼять на 200 000 карток для чотирьох варіантів:
   звичайний `@dataclass`, `@dataclass(slots=True)`, `NamedTuple` і звичайний клас
   із `__init__`. Побудуй табличку «байтів на обʼєкт» і випиши висновок одним реченням.
5. Знайди й опиши випадок, коли `NamedTuple` **не підходить**, а `dataclass` підходить,
   і підкріпи його кодом на пʼять рядків.

**Зроблено, якщо:**

- є `assert` на те, що `frozen` не захистив вкладений список — з коментарем, чому так;
- є `assert "list" in текст_помилки` для випадку з нехешованим полем;
- проходить `assert len({к1, к2}) == 1` для двох однакових карток із кортежем;
- у табличці памʼяті версія зі `slots=True` менша за звичайну щонайменше на 20 %,
  і числа отримані виміром, а не переписані з лекції;
- у пункті 5 названо конкретну можливість (наприклад, перевірка при створенні або
  зміна поля після створення) і показано код, який на `NamedTuple` не працює.

---

## Підказки

- **Рівень 1.** Якщо помилка про `mutable default` не зʼявилась — перевір, чи справді
  над класом стоїть `@dataclass`. Без декоратора це просто атрибут класу, і мовчазна
  пастка з теми 25 повертається.
- **Рівень 2.** `field(init=False)` без значення за замовчуванням цілком законний, поки
  `__post_init__` це поле заповнює. Але якщо перевірка кине виняток раніше за
  присвоєння, поля в обʼєкті так і не буде — це нормально, обʼєкт усе одно нікому
  не дістанеться.
- **Рівень 3.** Замок стоїть на **звʼязуванні** «імʼя поля → обʼєкт», а не на самому
  обʼєкті. Згадай тему 03: `frozen` забороняє переклеїти ярлик, а не заглядати
  в коробку.
- **Рівень 3, вимірювання.** `sys.getsizeof(обʼєкт)` не рахує словник атрибутів, тож
  сам по собі бреше. Або додавай `sys.getsizeof(обʼєкт.__dict__)`, або міряй
  `tracemalloc` на великій кількості обʼєктів — так, як у практиці.
- Перевіряти зручно на коротких іменах: `Ч("а", 1998, "к")`. Механізм той самий,
  а друкувати менше.
