# Домашнє завдання · Спискові включення

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

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

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

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

---

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

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

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

1. `назви_великими` — усі назви великими буквами;
2. `довжини_назв` — довжина кожної назви;
3. `ціни_з_пдв` — кожна ціна, помножена на 1.2 й округлена до копійок.

**Зроблено, якщо:** для кожного з трьох ти написав ще й цикл із `append` і виконуються

```python
assert назви_великими == назви_великими_циклом
assert довжини_назв == довжини_назв_циклом
assert ціни_з_пдв == ціни_з_пдв_циклом
assert len(назви_великими) == len(довжини_назв) == len(покупки)
```

Останній `assert` — не формальність: він фіксує, що включення **без умови** довжину
не змінює.

### 1.2 · Фільтр і його крайні випадки

Одним включенням відбери позиції, дорожчі за середню ціну кошика. Потім, не міняючи
запису, підстав межу, яку не проходить **ніхто**, і межу, яку проходять **усі**.

**Зроблено, якщо:** для трьох меж ти надрукував результат і його довжину, і виконуються

```python
assert len(дорожчі) < len(цінник)
assert [ц for ц in цінник if ц > 10 ** 9] == []
assert [ц for ц in цінник if ц > 0] == цінник
```

і ти письмово відповів одним реченням, чому порожній список тут — не помилка.

### 1.3 · Три дужки на одних даних

Візьми список слів, у якому **є повтори** (щонайменше два однакові слова). Побудуй із
нього три речі одним і тим самим виразом `len(слово)`, міняючи лише дужки: список,
множину й словник «слово → довжина».

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

```python
assert len(як_список) == len(слова)
assert len(як_множина) < len(як_список), "множина мусить злипнути однакові довжини"
assert len(як_словник) == len(set(слова)), "у словнику стільки записів, скільки різних слів"
assert set(як_список) == як_множина
```

і ти пояснив у коментарі, чому в словнику записів менше, ніж слів у списку.

---

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

### 2.1 · Фільтр проти тернара на одних даних

Візьми список оцінок від 0 до 100, щонайменше на **десять** значень, серед яких є і
низькі, і високі. Напиши два включення:

- `склали` — лише ті оцінки, що ≥ 60 (фільтр);
- `з_підтягуванням` — усі оцінки, але ті, що < 60, замінені на 60 (тернар).

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

```python
assert len(склали) < len(оцінки), "фільтр мусить скорочувати"
assert len(з_підтягуванням) == len(оцінки), "тернар мусить зберігати довжину"
assert min(з_підтягуванням) >= 60
assert sum(з_підтягуванням) > sum(оцінки)
```

і ти окремо надрукував, скільки оцінок фільтр **викинув** і скільки тернар **замінив** —
це має бути одне й те саме число. Поясни письмово, чому воно однакове, хоча довжини
результатів різні.

### 2.2 · Словник із двох списків і його перевертання

З двох паралельних списків (назви й кількості) збери словник `склад` **словниковим
включенням** через `zip`. Потім переверни його: побудуй `навпаки`, де ключами стануть
кількості.

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

```python
assert склад == dict(zip(покупки, кількості))
assert len(навпаки) <= len(склад)
```

і ти письмово відповів на два питання: (1) чому в другому `assert` стоїть `<=`, а не `==`;
(2) яка саме позиція «виживає», коли два ключі збігаються. Підтверди відповідь прикладом,
у якому `len(навпаки)` справді менша за `len(склад)`.

### 2.3 · Розплющити й зібрати назад

Дано матрицю 4 × 3 (чотири рядки по три числа). Одним вкладеним включенням розплющ її
в плаский список. Потім, **не використовуючи** оригінал, збери з плаского списку матрицю
назад — теж включенням, зі зрізами.

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

```python
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 · Побічний ефект: знайди й виправ

Ось код, який «працює»:

```python
рахунки = [120, 45, 300, 78]
[print("рахунок на", сума, "грн") for сума in рахунки]
```

**Зроблено, якщо:** ти (1) надрукував, що саме повертає це включення й скільки в ньому
елементів; (2) переписав його правильно; (3) виконав

```python
сміття = [print("рахунок на", сума, "грн") for сума in рахунки]
assert сміття == [None, None, None, None]
```

і пояснив двома реченнями, у чому саме шкода такого запису — крім витраченої памʼяті.

---

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

### 3.1 · Монстр і три його переписування

Дано список чеків, де кожен чек — список пар `(ціна, кількість)`:

```python
чеки = [[(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. двома-трьома простими включеннями з проміжними змінними, які мають осмислені імена.

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

```python
assert версія_1 == версія_2 == версія_3 == [68.4, 71.64, 76.8, 475.2]
```

а також ти навів **у символах** довжину найдовшого рядка кожної версії, порахував для
першої версії «рухомі частини» (`for` + `if` + виклики + розпакування) і письмово
обґрунтував, яку версію взяв би в реальний проєкт. Відповідь «першу, бо коротша»
приймається лише разом із поясненням, чому шість рухомих частин тебе не бентежать.

### 3.2 · Своє «розплющення» будь-якої глибини — без рекурсії

Включення вміє розплющити список списків. А що, як глибина невідома —
`[1, [2, 3], [4, [5, 6]], 7]`? Одним включенням це не робиться, і це важливий висновок.

Напиши код, який розплющує такий список **звичайними циклами** (рекурсію ми ще не вчили —
скористайся списком-«стосом», з якого береш елемент і, якщо це список, повертаєш назад
його вміст).

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

```python
вхід = [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`).

Заміряй кожен спосіб щонайменше пʼять разів і бери **найменший** час: одиничний замір
шумний, і на ньому можна «довести» будь-що.

**Зроблено, якщо:** ти надрукував три часи й два співвідношення, і виконуються

```python
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` у змінну
  може не дати нічого — або навіть трохи програти. Це нормальна відповідь: завдання
  просить заміряти й пояснити, а не підтвердити заздалегідь відому тезу.
- **Порожній вхід — перше, що варто перевірити.** Порожній список, фільтр, який нікого не
  пропускає, матриця без рядків: у всіх трьох випадках включення дає порожній результат
  і жодної помилки. Код має це пережити.
