Предметна область тут інша, ніж у лекції, — навмисно: якщо ти зможеш повторити ту саму
механіку на своїх даних, значить, справа не в завченому прикладі. Працюємо з
показаннями датчика температури: довгий потік чисел, який не влазить у памʼять
і який цікавий не весь, а по шматочках. Усе роби в окремому файлі datchyk.py або в
новому зошиті — практику не редагуй, вона знадобиться для звірки.
рівень 1Рівень 1 — База
Напиши те саме джерело двічі — класом і генератором — і доведи, що вони нерозрізненні.
- Клас
Показанняз методами__iter__і__next__: віддаєскількизначень температури, обчислюючи кожне за формулою20 + (номер * 37) % 15(жодної випадковості — числа мають повторюватись у всіх). - Генераторна функція
показання(скільки), яка робить рівно те саме черезyield. - Порахуй і надрукуй, скільки рядків коду вийшло в кожному варіанті.
- Заміряй
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Рівень 2 — Плюс
Збудуй лінивий конвеєр із трьох ланок і доведи лічильниками, що зайвої роботи немає.
- Три генератори:
показання(скільки)→у_фаренгейтах(значення)→лише_спека(значення, поріг=90). Кожна ланка бере попередню як аргумент. - У кожну ланку встав лічильник викликів (наприклад, у спільний словник
лічильник = {"джерело": 0, "переведено": 0, "віддано": 0}). - Попроси у конвеєра перші пʼять значень і зупинись. Надрукуй лічильники.
- Поруч зроби чесний варіант через списки: спершу весь список показань, потім весь список у Фаренгейтах, потім фільтр. Надрукуй його лічильники теж.
- Порівняй
tracemalloc-ом пік памʼяті обох варіантів на мільйоні показань.
Зроблено, якщо:
- проходить
assert результат_ліниво == результат_списками[:5]— конвеєр дає той самий початок, що й чесний варіант; - проходить
assert лічильник["джерело"] < 100для лінивого варіанта на мільйоні показань — тобто прочитано десятки значень, а не мільйон; - проходить
assert лічильник_списками["джерело"] == 1_000_000— і поруч коментар, чому саме так; - у файлі є два виміряні числа піку памʼяті й один рядок висновку, у скільки разів лінивий варіант дешевший.
рівень 3Рівень 3 — Виклик
Досліди межу лінощів: де вона економить, а де починає коштувати дорожче.
- Зроби клас
Датчик, чий__iter__щоразу створює новий генератор. Доведи трьомаassert-ами: (а) дваforпоспіль дають однакові списки, (б) обхід не будує список у памʼяті (sys.getsizeof(iter(датчик)) < 1000), (в) а класПоказанняз рівня 1 на другому обході дає порожньо. - Спіймай і запиши дослівно тексти трьох помилок:
len(генератор),генератор[0],reversed(генератор). Для кожної напиши одним реченням, чому інакше й бути не могло. - Візьми
itertools.tee(показання(1_000_000), 2), вичерпай одну копію повністю, а другу не чіпай — і заміряйtracemalloc-ом, скільки памʼяті це коштувало. Поясни числом, чомуteeне безкоштовний. - Знайди й покажи кодом випадок, коли список швидший за генератор: заміряй
time.perf_counterдляsumпо списку й по генератору на 10, 1 000 і 1 000 000 елементів. Побудуй табличку з трьох рядків. - Напиши генератор
ковзне_середнє(значення, вікно), який віддає середнє останніхвікнопоказань, не тримаючи в памʼяті більше завікночисел. Доведи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=вікно)сама викидає найстаріше значення, коли додаєш нове. Це рівно те, що потрібно, і воно не росте.