Домашнє завдання

Спискові включення

Три рівні. Кожен наступний спирається на попередній, але братися можна за будь-який. Пиши в окремому файлі homework_13.py або в новому зошиті — головне, щоб код запускався й друкував результат.

Дозволено з бібліотек: math, sys, timeit, collections. Більше нічого не треба. Функцій (тема 14) ми ще не проходили, тому lambda, map, filter і sorted(key=…) у розвʼязках не використовуй — усе робиться включеннями й циклами.

Наскрізне правило цього ДЗ: кожне включення, яке ти пишеш, супроводжуй еквівалентним циклом і перевіркою assert включення == цикл. Саме це доводить, що ти зрозумів запис, а не підібрав його навмання.


рівень 1Рівень 1 — База

1.1 · Свій кошик, три перетворення

Створи власний список покупок (щонайменше шість позицій), словник цін до нього й список кількостей тієї самої довжини. Потім трьома окремими включеннями побудуй:

  1. назви_великими — усі назви великими буквами;
  2. довжини_назв — довжина кожної назви;
  3. ціни_з_пдв — кожна ціна, помножена на 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, щонайменше на десять значень, серед яких є і низькі, і високі. Напиши два включення:

Зроблено, якщо: виконуються

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, по всіх чеках одразу. Напиши три версії:

  1. одним включенням із двома for і двома if;
  2. звичайними вкладеними циклами;
  3. двома-трьома простими включеннями з проміжними змінними, які мають осмислені імена.

Зроблено, якщо: виконуються

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) Чому виграш включення все одно не є достатньою причиною переписувати наявні цикли.


Підказки