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

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

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

---

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

Напиши те саме джерело двічі — класом і генератором — і доведи, що вони нерозрізненні.

1. Клас `Показання` з методами `__iter__` і `__next__`: віддає `скільки` значень
   температури, обчислюючи кожне за формулою
   `20 + (номер * 37) % 15` (жодної випадковості — числа мають повторюватись у всіх).
2. Генераторна функція `показання(скільки)`, яка робить рівно те саме через `yield`.
3. Порахуй і надрукуй, скільки рядків коду вийшло в кожному варіанті.
4. Заміряй `sys.getsizeof` для `показання(1_000_000)` і для
   `list(показання(1_000_000))` і надрукуй обидва числа з підписами.

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

- проходить `assert list(Показання(20)) == list(показання(20))` — обидва джерела дають
  однакову послідовність;
- проходить `assert list(показання(5)) == [20, 27, 34, 26, 33]` — тобто формула
  реалізована саме та;
- проходить `assert sys.getsizeof(показання(1_000_000)) < 1000` — генератор на мільйоні
  займає менш ніж кілобайт;
- у файлі є коментар із двома числами: рядків у класі й рядків у генераторі.

---

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

Збудуй лінивий конвеєр із трьох ланок і **доведи лічильниками**, що зайвої роботи немає.

1. Три генератори: `показання(скільки)` → `у_фаренгейтах(значення)` →
   `лише_спека(значення, поріг=90)`. Кожна ланка бере попередню як аргумент.
2. У кожну ланку встав лічильник викликів (наприклад, у спільний словник
   `лічильник = {"джерело": 0, "переведено": 0, "віддано": 0}`).
3. Попроси у конвеєра **перші пʼять** значень і зупинись. Надрукуй лічильники.
4. Поруч зроби чесний варіант через списки: спершу весь список показань, потім весь
   список у Фаренгейтах, потім фільтр. Надрукуй його лічильники теж.
5. Порівняй `tracemalloc`-ом пік памʼяті обох варіантів на мільйоні показань.

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

- проходить `assert результат_ліниво == результат_списками[:5]` — конвеєр дає той самий
  початок, що й чесний варіант;
- проходить `assert лічильник["джерело"] < 100` для лінивого варіанта на мільйоні
  показань — тобто прочитано десятки значень, а не мільйон;
- проходить `assert лічильник_списками["джерело"] == 1_000_000` — і поруч коментар,
  чому саме так;
- у файлі є два виміряні числа піку памʼяті й один рядок висновку, у скільки разів
  лінивий варіант дешевший.

---

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

Досліди межу лінощів: де вона економить, а де починає коштувати дорожче.

1. Зроби клас `Датчик`, чий `__iter__` **щоразу створює новий генератор**. Доведи
   трьома `assert`-ами: (а) два `for` поспіль дають однакові списки, (б) обхід не
   будує список у памʼяті (`sys.getsizeof(iter(датчик)) < 1000`), (в) а клас
   `Показання` з рівня 1 на другому обході дає порожньо.
2. Спіймай і запиши дослівно тексти трьох помилок: `len(генератор)`,
   `генератор[0]`, `reversed(генератор)`. Для кожної напиши одним реченням, чому
   інакше й бути не могло.
3. Візьми `itertools.tee(показання(1_000_000), 2)`, вичерпай **одну** копію повністю,
   а другу не чіпай — і заміряй `tracemalloc`-ом, скільки памʼяті це коштувало.
   Поясни числом, чому `tee` не безкоштовний.
4. Знайди й покажи кодом випадок, коли **список швидший за генератор**: заміряй
   `time.perf_counter` для `sum` по списку й по генератору на 10, 1 000 і 1 000 000
   елементів. Побудуй табличку з трьох рядків.
5. Напиши генератор `ковзне_середнє(значення, вікно)`, який віддає середнє останніх
   `вікно` показань, не тримаючи в памʼяті більше за `вікно` чисел. Доведи
   `assert`-ом на коротких даних, що результат збігається з наївним розрахунком
   через зрізи списку.

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

- усі три `assert`-и з пункту 1 проходять, і в коментарі названо рядок, яким
  `Датчик` відрізняється від `Показання`;
- у файлі є три дослівні тексти помилок із пункту 2, кожен із поясненням;
- вимір із пункту 3 показує щонайменше кілька мегабайтів, і поруч є речення про те,
  що `tee` буферизує все, що прочитала одна копія й не прочитала друга;
- у табличці з пункту 4 є хоча б один розмір, де список **виграв**, і цей результат
  прокоментовано;
- `ковзне_середнє` проходить `assert` на збіг із наївним розрахунком і при цьому
  ніде не викликає `list()` від вхідних даних.

---

## Підказки

- **Рівень 1.** Найкоротший спосіб порахувати рядки — виділити клас і функцію в окремі
  файли й подивитись у `wc -l`. Але цікаве не саме число, а те, скільки з цих рядків
  стосуються задачі, а не бухгалтерії позицій.
- **Рівень 2.** Лічильник має стояти в тілі генератора, а не поруч із ним: саме тіло
  виконується ліниво. Якщо цифри вийшли рівні мільйону, найімовірніша причина —
  десь у ланцюжку вставився `list(...)`, і вся лінь на ньому й скінчилась.
- **Рівень 3, пункт 1.** Різниця між класом-джерелом і класом-ітератором — рівно в
  тому, що повертає `__iter__`: `self` чи новий генератор. Одна відмінність в одному
  рядку міняє все.
- **Рівень 3, пункт 3.** `tee` не вміє передбачати майбутнє: щоб віддати другій копії
  те, що вже прочитала перша, вона мусить це десь зберегти. Черга всередині —
  звичайна `deque`.
- **Рівень 3, пункт 4.** Кожне відновлення генератора коштує кілька інструкцій. Там,
  де елементів десяток, ця плата помітніша за економію памʼяті — і саме тому
  «завжди пиши генератор» є поганою порадою.
- **Рівень 3, пункт 5.** `collections.deque(maxlen=вікно)` сама викидає найстаріше
  значення, коли додаєш нове. Це рівно те, що потрібно, і воно не росте.
