# Домашнє завдання · Декоратори

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

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

Спільна заготовка для всіх трьох рівнів:

```python
import time

КАТАЛОГ = {
    "А-101": "Кобзар",
    "Б-204": "Місто",
    "В-307": "Тигролови",
    "Г-412": "Записки українського самашедшого",
    "Д-518": "Інтернат",
}

def знайти_книгу(шифр):
    """Умовно повільний пошук у каталозі: 30 мс на звернення."""
    time.sleep(0.03)
    return КАТАЛОГ[шифр]
```

---

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

Напиши декоратор `@журнал`, який записує кожен виклик функції у спільний список, і
почепи його на `знайти_книгу`.

1. Обгортка має приймати `*args, **kwargs` і повертати результат **незміненим**.
2. У список `ЖУРНАЛ` клади кортеж `(імʼя_функції, аргументи, результат)`.
3. Обовʼязково постав `@wraps(func)`.
4. Зроби **дві** версії декоратора — з `@wraps` і без нього — і застосуй їх до двох
   однакових функцій, щоб порівняти службові поля.

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

- проходить `assert знайти_книгу("А-101") == "Кобзар"` — результат не змінився;
- проходить `assert len(ЖУРНАЛ) == 3` після трьох викликів, і в записах видно
  правильні шифри;
- проходить `assert знайти_книгу.__name__ == "знайти_книгу"` для версії з `wraps`;
- проходить `assert без_wraps.__name__ == "обгортка"` для версії без нього — і в
  коментарі поруч написано **одним реченням**, чому це шкодить.

---

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

Напиши декоратор з аргументом `@не_довше(секунд=0.05)`, який міряє час роботи функції
і, якщо вона працювала довше за дозволене, друкує попередження. Результат при цьому
має повертатись **завжди** — декоратор не має права ковтати відповідь.

1. Три рівні вкладеності: `не_довше(секунд)` → `декоратор(func)` → `обгортка(*args, **kwargs)`.
2. Перевір його на двох функціях: швидкій (без `sleep`) і повільній
   (`знайти_книгу` зі `sleep(0.03)`), а також на межі — зроби функцію зі `sleep(0.08)`.
3. Дай декоратору другий параметр `тихо=False`: якщо `True`, попередження не друкується,
   а лише додається у список `ПЕРЕВИЩЕННЯ`.
4. Запиши в коментарі розгортку цього декоратора без синтаксичного цукру — рядком.

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

- проходить `assert швидка(2) == 4` і `assert повільна("А-101") == "Кобзар"` — обидві
  функції повертають те, що мають;
- три `assert`-и перевіряють, що попередження зʼявилось саме для повільної функції і
  **не** зʼявилось для швидкої;
- проходить `assert len(ПЕРЕВИЩЕННЯ) == 1` для версії з `тихо=True`;
- у коментарі є рядок виду `повільна = не_довше(секунд=0.05)(повільна)`;
- проходить `assert повільна.__name__ == "повільна"` — `@wraps` на місці.

---

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

Напиши власний LRU-кеш і доведи, що він поводиться **точно** так, як бібліотечний.

1. Декоратор `@мій_lru(maxsize=N)`: словник `аргументи → результат` плюс список
   порядку звернень. При переповненні викидай найдавніше вживаний запис.
2. Додай метод `статистика()`, що повертає кортеж `(влучань, промахів, розмір)` —
   як `cache_info()` у бібліотеки.
3. Візьми послідовність із **щонайменше 40 запитів** до пʼяти шифрів (склади її сам,
   з повторами) і прожени її двічі: через свій декоратор і через
   `functools.lru_cache(maxsize=N)`.
4. Порівняй `hits` і `misses` для `maxsize` = 1, 2, 3, 5 — усі чотири випадки.
5. Зміряй час обох варіантів і побудуй табличку «maxsize · влучань · промахів · час».
6. Додай **один** випадок, де твій кеш і бібліотечний розійшлися б, якби ти
   реалізував витіснення неправильно (наприклад, викидав найстаріший **доданий**,
   а не найдавніше **вживаний**), і покажи це числами.

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

- проходить `assert мої_відповіді == бібліотечні_відповіді` — значення однакові;
- для всіх чотирьох `maxsize` проходить
  `assert моя_статистика[:2] == (info.hits, info.misses)`;
- у табличці видно, що при `maxsize=5` промахів рівно стільки, скільки різних шифрів,
  і що менше вже не буває;
- пункт 6 підкріплений числами: наведено послідовність, на якій FIFO і LRU дають
  **різну** кількість промахів, і різниця показана в коді;
- проходить `assert знайти_книгу.__name__ == "знайти_книгу"` — свій декоратор теж
  користується `@wraps`.

---

## Підказки

- **Рівень 1.** Якщо `ЖУРНАЛ` виявився порожнім — перевір, чи не забув ти повернути
  обгортку з декоратора: `return обгортка` без дужок. З дужками Python спробує
  викликати її на місці й одразу впаде.
- **Рівень 2.** Три рівні легше писати згори вниз, а читати — знизу вгору. Спочатку
  напиши звичайний декоратор без параметрів, переконайся, що він працює, і лише потім
  обгорни його ще однією функцією, яка приймає налаштування.
- **Рівень 2, друга пастка.** `@не_довше` без дужок не дасть помилки в цьому рядку —
  функція просто поїде в параметр `секунд`. Зламається все пізніше й в іншому місці.
- **Рівень 3.** LRU відрізняється від FIFO одним рухом: при **влучанні** запис теж
  переїжджає в кінець списку порядку. Забудеш це — отримаєш чергу, а не LRU.
- **Рівень 3, вимірювання.** Не порівнюй час одного виклику: `sleep` дає похибку.
  Міряй увесь прогін послідовності `time.perf_counter()`, як у практиці.
- Перевіряти зручно на коротких шифрах і `sleep(0.01)` — механізм той самий, а чекати
  втричі менше.
