Три рівні. Кожен наступний спирається на попередній, але братися можна за будь-який.
Пиши в окремому файлі homework_13.py або в новому зошиті — головне, щоб код
запускався й друкував результат.
Дозволено з бібліотек: math, sys, timeit, collections. Більше нічого не треба.
Функцій (тема 14) ми ще не проходили, тому lambda, map, filter і sorted(key=…)
у розвʼязках не використовуй — усе робиться включеннями й циклами.
Наскрізне правило цього ДЗ: кожне включення, яке ти пишеш, супроводжуй еквівалентним
циклом і перевіркою assert включення == цикл. Саме це доводить, що ти зрозумів запис,
а не підібрав його навмання.
рівень 1Рівень 1 — База
1.1 · Свій кошик, три перетворення
Створи власний список покупок (щонайменше шість позицій), словник цін до нього й список кількостей тієї самої довжини. Потім трьома окремими включеннями побудуй:
назви_великими— усі назви великими буквами;довжини_назв— довжина кожної назви;ціни_з_пдв— кожна ціна, помножена на 1.2 й округлена до копійок.
Зроблено, якщо: для кожного з трьох ти написав ще й цикл із append і виконуються
assert назви_великими == назви_великими_циклом
assert довжини_назв == довжини_назв_циклом
assert ціни_з_пдв == ціни_з_пдв_циклом
assert len(назви_великими) == len(довжини_назв) == len(покупки)
Останній assert — не формальність: він фіксує, що включення без умови довжину
не змінює.
1.2 · Фільтр і його крайні випадки
Одним включенням відбери позиції, дорожчі за середню ціну кошика. Потім, не міняючи запису, підстав межу, яку не проходить ніхто, і межу, яку проходять усі.
Зроблено, якщо: для трьох меж ти надрукував результат і його довжину, і виконуються
assert len(дорожчі) < len(цінник)
assert [ц for ц in цінник if ц > 10 ** 9] == []
assert [ц for ц in цінник if ц > 0] == цінник
і ти письмово відповів одним реченням, чому порожній список тут — не помилка.
1.3 · Три дужки на одних даних
Візьми список слів, у якому є повтори (щонайменше два однакові слова). Побудуй із
нього три речі одним і тим самим виразом len(слово), міняючи лише дужки: список,
множину й словник «слово → довжина».
Зроблено, якщо: виконуються
assert len(як_список) == len(слова)
assert len(як_множина) < len(як_список), "множина мусить злипнути однакові довжини"
assert len(як_словник) == len(set(слова)), "у словнику стільки записів, скільки різних слів"
assert set(як_список) == як_множина
і ти пояснив у коментарі, чому в словнику записів менше, ніж слів у списку.
рівень 2Рівень 2 — Плюс
2.1 · Фільтр проти тернара на одних даних
Візьми список оцінок від 0 до 100, щонайменше на десять значень, серед яких є і низькі, і високі. Напиши два включення:
склали— лише ті оцінки, що ≥ 60 (фільтр);з_підтягуванням— усі оцінки, але ті, що < 60, замінені на 60 (тернар).
Зроблено, якщо: виконуються
assert len(склали) < len(оцінки), "фільтр мусить скорочувати"
assert len(з_підтягуванням) == len(оцінки), "тернар мусить зберігати довжину"
assert min(з_підтягуванням) >= 60
assert sum(з_підтягуванням) > sum(оцінки)
і ти окремо надрукував, скільки оцінок фільтр викинув і скільки тернар замінив — це має бути одне й те саме число. Поясни письмово, чому воно однакове, хоча довжини результатів різні.
2.2 · Словник із двох списків і його перевертання
З двох паралельних списків (назви й кількості) збери словник склад словниковим
включенням через zip. Потім переверни його: побудуй навпаки, де ключами стануть
кількості.
Зроблено, якщо: виконуються
assert склад == dict(zip(покупки, кількості))
assert len(навпаки) <= len(склад)
і ти письмово відповів на два питання: (1) чому в другому assert стоїть <=, а не ==;
(2) яка саме позиція «виживає», коли два ключі збігаються. Підтверди відповідь прикладом,
у якому len(навпаки) справді менша за len(склад).
2.3 · Розплющити й зібрати назад
Дано матрицю 4 × 3 (чотири рядки по три числа). Одним вкладеним включенням розплющ її в плаский список. Потім, не використовуючи оригінал, збери з плаского списку матрицю назад — теж включенням, зі зрізами.
Зроблено, якщо: виконуються
assert пласкі == [значення for рядок in матриця for значення in рядок]
assert len(пласкі) == 12
assert зібрана_назад == матриця
assert all(зібрана_назад[i] is not матриця[i] for i in range(4))
Підказка до другої частини: зріз пласкі[i:i + 3] дає один рядок; лишилось перебрати
потрібні i.
2.4 · Побічний ефект: знайди й виправ
Ось код, який «працює»:
рахунки = [120, 45, 300, 78]
[print("рахунок на", сума, "грн") for сума in рахунки]
Зроблено, якщо: ти (1) надрукував, що саме повертає це включення й скільки в ньому елементів; (2) переписав його правильно; (3) виконав
сміття = [print("рахунок на", сума, "грн") for сума in рахунки]
assert сміття == [None, None, None, None]
і пояснив двома реченнями, у чому саме шкода такого запису — крім витраченої памʼяті.
рівень 3Рівень 3 — Виклик
3.1 · Монстр і три його переписування
Дано список чеків, де кожен чек — список пар (ціна, кількість):
чеки = [[(28.5, 2), (145.0, 1)],
[(19.9, 3), (12.0, 1)],
[(32.0, 2), (99.0, 4)]]
Треба порахувати вартість із націнкою 20% для тих позицій, де ціна < 100 і кількість > 1, по всіх чеках одразу. Напиши три версії:
- одним включенням із двома
forі двомаif; - звичайними вкладеними циклами;
- двома-трьома простими включеннями з проміжними змінними, які мають осмислені імена.
Зроблено, якщо: виконуються
assert версія_1 == версія_2 == версія_3 == [68.4, 71.64, 76.8, 475.2]
а також ти навів у символах довжину найдовшого рядка кожної версії, порахував для
першої версії «рухомі частини» (for + if + виклики + розпакування) і письмово
обґрунтував, яку версію взяв би в реальний проєкт. Відповідь «першу, бо коротша»
приймається лише разом із поясненням, чому шість рухомих частин тебе не бентежать.
3.2 · Своє «розплющення» будь-якої глибини — без рекурсії
Включення вміє розплющити список списків. А що, як глибина невідома —
[1, [2, 3], [4, [5, 6]], 7]? Одним включенням це не робиться, і це важливий висновок.
Напиши код, який розплющує такий список звичайними циклами (рекурсію ми ще не вчили — скористайся списком-«стосом», з якого береш елемент і, якщо це список, повертаєш назад його вміст).
Зроблено, якщо: виконуються
вхід = [1, [2, 3], [4, [5, 6]], 7]
assert розплющене == [1, 2, 3, 4, 5, 6, 7]
assert розплющене != вхід, "результат мусить бути плаский, а не той самий список"
і ти письмово пояснив у 2–3 реченнях, чому включення тут не підходить: яка саме властивість запису робить його непридатним для невідомої глибини.
Перевір ще на трьох входах: [] → [], [[[[1]]]] → [1], [1, 2, 3] → [1, 2, 3].
3.3 · Заміряй і не повір на слово
Заміряй timeit-ом на списку з 200 000 чисел три способи побудувати список подвоєних
значень: включення, цикл із append і цикл із наперед збереженим методом
(дод = результат.append).
Заміряй кожен спосіб щонайменше пʼять разів і бери найменший час: одиничний замір шумний, і на ньому можна «довести» будь-що.
Зроблено, якщо: ти надрукував три часи й два співвідношення, і виконуються
assert включення_час < цикл_час, "включення мусить бути швидшим за цикл із append"
assert цикл_час / включення_час < 3, "різниця має бути в десятки відсотків, а не в рази"
і ти письмово відповів на два питання. (1) Третій спосіб — це класична порада зі старих посібників: «збережи метод у змінну, щоб не шукати його щоразу». Перевір, чи справді він швидший за другий на твоєму Python, і поясни результат. (2) Чому виграш включення все одно не є достатньою причиною переписувати наявні цикли.
Підказки
- Пиши цикл першим, включення другим. Так набагато легше не помилитись: спершу
розвʼязуєш задачу звичним способом, потім переносиш частини на нові місця, а
assertловить розбіжність одразу. else— це маркер тернара. Побачивelse— він мусить стояти передfor. Немаєelse—ifіде післяfor. Іншого варіанта не буває.- У 2.2 подумай про два однакові значення. Що станеться з ключем, якщо дві позиції
мають однакову кількість? Саме там і ховається різниця між
<=і==. - У 2.3 не змінюй матрицю на місці. Перевірка
is notдивиться на кожен рядок окремо: зрізпласкі[i:i + 3]створює новий список, тому вона пройде — але лише якщо ти справді збирав матрицю назад, а не присвоїв оригінал. Усерединіall(...)тут стоїть генераторний вираз із розділу 09 лекції. - У 3.1 порахуй рухомі частини чесно.
for-и,if-и, кожен виклик функції (round,zip) і розпакуванняціна, скільки— усе разом. Від чотирьох варто зупинитись і подумати. - У 3.2 стос — це звичайний список. Береш останній елемент через
.pop(); якщо це список, доклади його вміст назад через.extend(); якщо ні — це готове значення. Порядок наприкінці доведеться перевернути. - У 3.3 будь готовий, що порада зі старих посібників не спрацює. З Python 3.11
інтерпретатор сам спеціалізує виклик методу, тому ручне збереження
appendу змінну може не дати нічого — або навіть трохи програти. Це нормальна відповідь: завдання просить заміряти й пояснити, а не підтвердити заздалегідь відому тезу. - Порожній вхід — перше, що варто перевірити. Порожній список, фільтр, який нікого не пропускає, матриця без рядків: у всіх трьох випадках включення дає порожній результат і жодної помилки. Код має це пережити.