Практика · Спискові включення¶

Лекція: lecture.html · Тест: quiz.html · ДЗ: homework.md

Наскрізний приклад той самий, що в лекції, — кошик покупок. Головне правило цього зошита: кожне включення ми звіряємо з еквівалентним циклом через assert. Не «схоже», не «майже те саме», а буквально той самий результат.

Що зробимо:

  1. перепишемо цикл із append на включення — і доведемо, що це те саме;
  2. перетворимо список: подвоєння, len, великі букви;
  3. відфільтруємо через if у кінці — і побачимо, що список коротшає;
  4. порівняємо фільтр із тернаром на початку, який довжину зберігає;
  5. зберемо словник із двох списків через zip і перевернемо наявний словник;
  6. розплющимо матрицю вкладеним включенням;
  7. розберемо [[0] * 3 for _ in range(3)] і чому [[0] * 3] * 3 зламане;
  8. подивимось на включення з побічним ефектом і на область видимості;
  9. напишемо свідомо погане включення й перепишемо його циклом;
  10. заміряємо швидкість і познайомимось із генераторним виразом.

1 · Дані, з якими працюємо¶

Ті самі чотири структури, що й у лекції. цінник — це просто ціни списком, у тому самому порядку, що й покупки: так із ними зручніше показувати включення.

In [1]:
покупки = ["хліб", "молоко", "яблука", "мед", "сіль"]
ціни = {"хліб": 28.5, "молоко": 32.0, "яблука": 19.9, "мед": 145.0, "сіль": 12.0}
кількості = [2, 1, 3, 1, 1]
цінник = [28.5, 32.0, 19.9, 145.0, 12.0]

print("покупки  :", покупки)
print("цінник   :", цінник)
print("кількості:", кількості)
print("позицій у кошику:", len(покупки))
покупки  : ['хліб', 'молоко', 'яблука', 'мед', 'сіль']
цінник   : [28.5, 32.0, 19.9, 145.0, 12.0]
кількості: [2, 1, 3, 1, 1]
позицій у кошику: 5

2 · Той самий алгоритм двома записами¶

Спершу пишемо звичайний цикл із append — рівно так, як робили в темі 12. Потім те саме включенням. І одразу перевіряємо assert-ом, що результати збігаються до останнього елемента: це головна перевірка всього зошита.

In [2]:
# спосіб 1 — цикл: створюємо порожній список і доповнюємо його на кожному кроці
подвоєні_циклом = []
for ціна in цінник:
    подвоєні_циклом.append(ціна * 2)

# спосіб 2 — включення: описуємо результат одним виразом
подвоєні_включенням = [ціна * 2 for ціна in цінник]

print("циклом     :", подвоєні_циклом)
print("включенням :", подвоєні_включенням)

# найважливіший рядок зошита: це не «схожі» списки, а буквально однакові
assert подвоєні_циклом == подвоєні_включенням, "включення дало не те, що цикл!"
print("✅ збігається: включення = той самий цикл")
циклом     : [57.0, 64.0, 39.8, 290.0, 24.0]
включенням : [57.0, 64.0, 39.8, 290.0, 24.0]
✅ збігається: включення = той самий цикл

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

In [3]:
цикл_рядків = 3          # подвоєні = [] · for ... · append(...)
включення_рядків = 1

print("рядків циклом     :", цикл_рядків)
print("рядків включенням :", включення_рядків)
print("довжина результату однакова:", len(подвоєні_циклом), "=", len(подвоєні_включенням))
рядків циклом     : 3
рядків включенням : 1
довжина результату однакова: 5 = 5

3 · Перетворення: вираз може бути будь-яким¶

У виразі не обовʼязково арифметика. Годиться виклик методу, виклик функції, звертання за індексом — усе, що повертає значення. Зверни увагу на третій приклад: на вході рядки, на виході числа. Тип результату не зобовʼязаний збігатися з типом джерела.

In [4]:
гучні = [назва.upper() for назва in покупки]
довжини = [len(назва) for назва in покупки]
перші_букви = [назва[0] for назва in покупки]

print("великими  :", гучні)
print("довжини   :", довжини)
print("перші букви:", перші_букви)

# джерело не постраждало: рядки незмінні, .upper() повертає НОВИЙ рядок
print("покупки лишились такими ж:", покупки)
великими  : ['ХЛІБ', 'МОЛОКО', 'ЯБЛУКА', 'МЕД', 'СІЛЬ']
довжини   : [4, 6, 6, 3, 4]
перші букви: ['х', 'м', 'я', 'м', 'с']
покупки лишились такими ж: ['хліб', 'молоко', 'яблука', 'мед', 'сіль']

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

In [5]:
довжини_циклом = []
for назва in покупки:
    довжини_циклом.append(len(назва))

assert довжини == довжини_циклом
assert len(довжини) == len(покупки), "без умови довжина мусить зберігатись"
print("✅ довжини збігаються, елементів однаково:", len(довжини))
✅ довжини збігаються, елементів однаково: 5

4 · Фільтр: if у кінці¶

Умова в кінці вирішує, чи потрапить елемент у результат. Це прямий аналог continue у звичайному циклі. Зверни увагу: вираз тут тривіальний (ціна), бо ми нічого не перетворюємо — ми відбираємо.

In [6]:
# цикл із вкладеним if
дешеві_циклом = []
for ціна in цінник:
    if ціна < 100:
        дешеві_циклом.append(ціна)

# те саме включенням: умова переїхала в хвіст того самого рядка
дешеві = [ціна for ціна in цінник if ціна < 100]

print("усі ціни :", цінник, "— елементів:", len(цінник))
print("дешеві   :", дешеві, "— елементів:", len(дешеві))

assert дешеві == дешеві_циклом
print("✅ фільтр включенням = фільтр циклом")
print("список СКОРОТИВСЯ на", len(цінник) - len(дешеві), "позицію — мед за 145.0 не пройшов")
усі ціни : [28.5, 32.0, 19.9, 145.0, 12.0] — елементів: 5
дешеві   : [28.5, 32.0, 19.9, 12.0] — елементів: 4
✅ фільтр включенням = фільтр циклом
список СКОРОТИВСЯ на 1 позицію — мед за 145.0 не пройшов

Умова може бути якою завгодно — усі правила істинності з теми 11 працюють без змін. Крайній випадок теж корисний: якщо умову не проходить ніхто, виходить порожній список. Це не помилка.

In [7]:
довгі_назви = [назва for назва in покупки if len(назва) > 4]
з_буквою_м = [назва for назва in покупки if "м" in назва]
неможливе = [ціна for ціна in цінник if ціна > 1000]

print("довші за 4 букви :", довгі_назви)
print("з буквою «м»     :", з_буквою_м)
print("дорожчі за 1000  :", неможливе, "— порожній список, і це законно")
довші за 4 букви : ['молоко', 'яблука']
з буквою «м»     : ['молоко', 'мед']
дорожчі за 1000  : [] — порожній список, і це законно

5 · Перетворення проти фільтра: тернар на початку¶

А тепер інша задача: не «залиш дешеві», а «обріж дорогі до 100». Кількість позицій має лишитись тією самою — ми нічого не викидаємо. Для цього потрібен тернарний вираз a if умова else b, і стоїть він на місці виразу, тобто перед for.

In [8]:
зі_стелею = [ціна if ціна < 100 else 100 for ціна in цінник]

print("джерело   :", цінник,      "— довжина", len(цінник))
print("фільтр    :", дешеві,      "— довжина", len(дешеві))
print("зі стелею :", зі_стелею,   "— довжина", len(зі_стелею))

# ключова різниця в одному рядку
assert len(дешеві) < len(цінник), "фільтр мусить скорочувати"
assert len(зі_стелею) == len(цінник), "тернар мусить зберігати довжину"
print("✅ фільтр скоротив, тернар зберіг довжину")
джерело   : [28.5, 32.0, 19.9, 145.0, 12.0] — довжина 5
фільтр    : [28.5, 32.0, 19.9, 12.0] — довжина 4
зі стелею : [28.5, 32.0, 19.9, 100, 12.0] — довжина 5
✅ фільтр скоротив, тернар зберіг довжину

Запамʼятати легко: else буває тільки в тернарі. Якщо в тебе є else, він мусить стояти перед for. Спроба поставити else у хвіст — це SyntaxError ще до запуску, тому такий код ми показуємо рядком, а не виконуємо.

In [9]:
поганий_запис = '[ціна for ціна in цінник if ціна < 100 else 0]'
добрий_запис  = '[ціна if ціна < 100 else 0 for ціна in цінник]'

print("так НЕ можна:", поганий_запис)
print("            ", "SyntaxError: expected 'else' after 'if' expression")
print("так можна   :", добрий_запис)
print("результат   :", [ціна if ціна < 100 else 0 for ціна in цінник])
так НЕ можна: [ціна for ціна in цінник if ціна < 100 else 0]
             SyntaxError: expected 'else' after 'if' expression
так можна   : [ціна if ціна < 100 else 0 for ціна in цінник]
результат   : [28.5, 32.0, 19.9, 0, 12.0]

6 · Три дужки — три типи¶

Той самий вираз len(назва) дає три різні речі залежно від дужок. Візьмемо список із повторами, щоб різниця була видима.

In [10]:
з_повторами = ["хліб", "мед", "сіль", "мед", "хліб"]

як_список = [len(назва) for назва in з_повторами]
як_множина = {len(назва) for назва in з_повторами}
як_словник = {назва: len(назва) for назва in з_повторами}

print("[...]    ->", як_список,  type(як_список).__name__,  "-", len(як_список), "елементів")
print("{...}    ->", як_множина, type(як_множина).__name__, "-", len(як_множина), "елементи")
print("{k: v}   ->", як_словник, type(як_словник).__name__, "-", len(як_словник), "пари")
[...]    -> [4, 3, 4, 3, 4] list - 5 елементів
{...}    -> {3, 4} set - 2 елементи
{k: v}   -> {'хліб': 4, 'мед': 3, 'сіль': 4} dict - 3 пари

Дублікати ми ніде не прибирали — це зробила сама природа типу: множина не вміє тримати два однакові елементи, а словник не вміє тримати два однакові ключі.

In [11]:
assert len(як_список) == 5, "список бере все підряд"
assert len(як_множина) == 2, "множина лишає лише різні значення"
assert len(як_словник) == 3, "у словнику стільки записів, скільки різних ключів"
assert як_множина == set(як_список), "множина — це ті самі значення без повторів"
print("✅ 5 -> 2 -> 3: дублікати злиплись самі, без жодного коду")
✅ 5 -> 2 -> 3: дублікати злиплись самі, без жодного коду

7 · Словник із двох списків через zip¶

Найкорисніший практичний рецепт цієї теми. Назви лежать в одному списку, кількості — в другому; словниковим включенням вони склеюються в один запис. Після for тут стоять два імені, бо zip віддає пари (розпакування з теми 07).

In [12]:
# цикл
склад_циклом = {}
for назва, скільки in zip(покупки, кількості):
    склад_циклом[назва] = скільки

# включення
склад = {назва: скільки for назва, скільки in zip(покупки, кількості)}

print("склад:", склад)

assert склад == склад_циклом
assert склад == dict(zip(покупки, кількості)), "це те саме, що вбудований dict(zip(...))"
print("✅ словникове включення = цикл = dict(zip(...))")
склад: {'хліб': 2, 'молоко': 1, 'яблука': 3, 'мед': 1, 'сіль': 1}
✅ словникове включення = цикл = dict(zip(...))

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

In [13]:
навпаки = {ціна: назва for назва, ціна in ціни.items()}

print("ціни    :", ціни)
print("навпаки :", навпаки)
print("записів було", len(ціни), "- стало", len(навпаки), "(усі ціни різні, тому нічого не злиплось)")

# порахуємо вартість кожної позиції — теж словниковим включенням
вартість = {назва: round(ціни[назва] * скільки, 2)
            for назва, скільки in склад.items()}
print("вартість:", вартість)
print("разом до сплати:", round(sum(вартість.values()), 2))
ціни    : {'хліб': 28.5, 'молоко': 32.0, 'яблука': 19.9, 'мед': 145.0, 'сіль': 12.0}
навпаки : {28.5: 'хліб', 32.0: 'молоко', 19.9: 'яблука', 145.0: 'мед', 12.0: 'сіль'}
записів було 5 - стало 5 (усі ціни різні, тому нічого не злиплось)
вартість: {'хліб': 57.0, 'молоко': 32.0, 'яблука': 59.7, 'мед': 145.0, 'сіль': 12.0}
разом до сплати: 305.7

8 · Вкладене включення: розплющуємо матрицю¶

Два for в одному включенні — це рівно вкладені цикли з теми 12, у тому самому порядку: зліва направо = зовні всередину. Поклади обидва записи поруч і прочитай — вони збігаються слово в слово.

In [14]:
матриця = [[1, 2, 3, 4],
           [5, 6, 7, 8],
           [9, 10, 11, 12]]

# вкладені цикли
пласкі_циклом = []
for рядок in матриця:
    for значення in рядок:
        пласкі_циклом.append(значення)

# те саме включенням — for-и в тому самому порядку
пласкі = [значення for рядок in матриця for значення in рядок]

print("матриця :", матриця)
print("пласкі  :", пласкі)

assert пласкі == пласкі_циклом
assert len(пласкі) == 3 * 4, "три рядки по чотири значення — рівно 12 кроків"
print("✅ розплющення включенням = вкладені цикли, елементів:", len(пласкі))
матриця : [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]
пласкі  : [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
✅ розплющення включенням = вкладені цикли, елементів: 12

Переставити for-и не можна: другий бере джерело з першого. Якщо поміняти їх місцями, імʼя рядок згадається раніше, ніж зʼявиться. Спіймаємо цю помилку й надрукуємо її текст — traceback теж навчальний матеріал.

Одна тонкість: після звичайного циклу вище імʼя рядок лишилось живим (це та сама властивість циклу з теми 12). Воно замаскувало б помилку, тому спершу приберемо його через del — і тоді побачимо чисту поведінку включення.

In [15]:
# після циклу вище імʼя «рядок» лишилось живим — прибираємо його,
# інакше воно замаскує помилку, і зламане включення випадково спрацює
del рядок

try:
    # свідомо неправильний порядок: перший for просить джерело, якого ще немає
    зламане = [значення for значення in рядок for рядок in матриця]
except NameError as помилка:
    print("NameError:", помилка)
    print("причина: перший for уже просить «рядок», а створює його лише другий for")
    print("порядок for-ів у включенні той самий, що й у вкладених циклах: зовні всередину")
NameError: name 'рядок' is not defined
причина: перший for уже просить «рядок», а створює його лише другий for
порядок for-ів у включенні той самий, що й у вкладених циклах: зовні всередину

9 · Розгадка [[0] * 3 for _ in range(3)]¶

Це замовляння з теми 06, яке ми двічі просили просто запамʼятати. Тепер його можна прочитати: «для кожного з трьох кроків поклади в список новий рядок [0] * 3». Ключове слово — новий: вираз обчислюється заново на кожному кроці.

А поруч — зламаний варіант [[0] * 3] * 3, де множення копіює посилання на один і той самий список.

In [16]:
правильна = [[0] * 3 for _ in range(3)]
зламана = [[0] * 3] * 3

# змінюємо одну клітинку в кожній таблиці й дивимось, що сталося
правильна[0][0] = 9
зламана[0][0] = 9

print("правильна:", правильна)
print("зламана  :", зламана, "<- девʼятка зʼявилась у ВСІХ трьох рядках")

# доказ: у зламаній усі три рядки — це один і той самий обʼєкт
print("правильна — різні обʼєкти:", id(правильна[0]) != id(правильна[1]))
print("зламана   — один обʼєкт  :", id(зламана[0]) == id(зламана[1]))
assert правильна == [[9, 0, 0], [0, 0, 0], [0, 0, 0]]
assert зламана == [[9, 0, 0], [9, 0, 0], [9, 0, 0]]
print("✅ ось навіщо тут включення: воно щоразу створює НОВИЙ рядок")
правильна: [[9, 0, 0], [0, 0, 0], [0, 0, 0]]
зламана  : [[9, 0, 0], [9, 0, 0], [9, 0, 0]] <- девʼятка зʼявилась у ВСІХ трьох рядках
правильна — різні обʼєкти: True
зламана   — один обʼєкт  : True
✅ ось навіщо тут включення: воно щоразу створює НОВИЙ рядок

Підкреслення _ — це звичайне імʼя змінної, а не синтаксис. Домовленість така: якщо значення нам не потрібне, а потрібна лише кількість повторень, називаємо змінну _.

In [17]:
три_привітання = ["привіт" for _ in range(3)]
номери_рядків = [номер for номер in range(3)]

print("значення не потрібне:", три_привітання)
print("значення потрібне   :", номери_рядків)
значення не потрібне: ['привіт', 'привіт', 'привіт']
значення потрібне   : [0, 1, 2]

10 · Включення з побічним ефектом — типова помилка¶

Класична помилка того, хто щойно вивчив включення: використати його заради дії, а не заради результату. Подивимось, що насправді збирається в такому «списку».

In [18]:
сміття = [print("у кошику:", назва) for назва in покупки]

print()
print("а що ж повернуло включення?", сміття)
print("тип елементів:", type(сміття[0]).__name__)
assert сміття == [None] * len(покупки), "print нічого не повертає, тобто повертає None"
print("✅ ми виділили памʼять під 5 None і одразу їх викинули")
у кошику: хліб
у кошику: молоко
у кошику: яблука
у кошику: мед
у кошику: сіль

а що ж повернуло включення? [None, None, None, None, None]
тип елементів: NoneType
✅ ми виділили памʼять під 5 None і одразу їх викинули

Правильно так: потрібна дія — пиши звичайний for. Дужки [...] обіцяють читачеві, що тут будують список, і цю обіцянку не можна порушувати.

In [19]:
for назва in покупки:
    print("у кошику:", назва)

print()
print("той самий вивід, але без списку з пʼятьох None")
у кошику: хліб
у кошику: молоко
у кошику: яблука
у кошику: мед
у кошику: сіль

той самий вивід, але без списку з пʼятьох None

11 · Область видимості: змінна не тече назовні¶

У звичайному циклі змінна після завершення лишається живою — ми бачили це в темі 12. У включенні вона зникає разом із дужками. Перевіримо обидва випадки поспіль.

In [20]:
# цикл: змінна лишається живою й тримає ОСТАННЄ значення
for перевірка in range(3):
    pass
print("після циклу перевірка =", перевірка)

# включення: власна область видимості
квадрати = [ч ** 2 for ч in range(3)]
print("квадрати =", квадрати)
try:
    print(ч)
except NameError as помилка:
    print("NameError:", помилка)
    print("імʼя «ч» жило тільки всередині квадратних дужок")
після циклу перевірка = 2
квадрати = [0, 1, 4]
NameError: name 'ч' is not defined
імʼя «ч» жило тільки всередині квадратних дужок

Практична користь від цього — включення не затирає змінну, яка вже була в тебе під тим самим іменем. У Python 2 було інакше: змінна витікала назовні, і це вважали помилкою дизайну.

In [21]:
ціна = "моя важлива змінна"
дрібниця = [ціна * 2 for ціна in цінник]     # усередині ціна — число

print("усередині включення ціна була числом:", дрібниця)
print("а зовні вона лишилась недоторканою  :", ціна)
assert ціна == "моя важлива змінна", "включення не мусить затирати зовнішнє імʼя"
print("✅ зовнішнє імʼя вціліло")
усередині включення ціна була числом: [57.0, 64.0, 39.8, 290.0, 24.0]
а зовні вона лишилась недоторканою  : моя важлива змінна
✅ зовнішнє імʼя вціліло

12 · Свідомо погане включення — і як його переписати¶

Тепер найважливіша вправа теми. Напишемо включення, яке технічно правильне, а читати його неможливо: два for, дві умови, виклик функції й розпакування всередині. Порахуємо «рухомі частини» — for-и, if-и, виклики й розпакування — і подивимось на довжину рядка.

In [22]:
чеки = [[(28.5, 2), (145.0, 1)],
        [(19.9, 3), (12.0, 1)],
        [(32.0, 2), (99.0, 4)]]

# ⚠️ так писати не треба — це приклад того, чого варто уникати
монстр = [round(ц * к * 1.2, 2) for чек in чеки for ц, к in чек if ц < 100 if к > 1]

рядок = '[round(ц * к * 1.2, 2) for чек in чеки for ц, к in чек if ц < 100 if к > 1]'
print("результат:", монстр)
print("довжина рядка:", len(рядок), "символів (межа читабельності — близько 72)")
print("рухомих частин: 2 for + 2 if + 1 виклик + 1 розпакування = 6")
результат: [68.4, 71.64, 76.8, 475.2]
довжина рядка: 75 символів (межа читабельності — близько 72)
рухомих частин: 2 for + 2 if + 1 виклик + 1 розпакування = 6

А тепер той самий алгоритм звичайними циклами. Він довший у рядках — шість замість одного, — але жоден із цих рядків не потребує паузи на розуміння. Саме це й означає правило «не читається вголос за один подих — пиши цикл».

In [23]:
результат = []
for чек in чеки:
    for ціна, скільки in чек:
        if ціна < 100 and скільки > 1:
            # націнка 20%, округлення до копійок — рахуємо окремим зрозумілим рядком
            результат.append(round(ціна * скільки * 1.2, 2))

print("циклом    :", результат, "— рядків коду: 6")
print("включенням:", монстр, "— рядків коду: 1")

assert результат == монстр, "переписування циклом мусить дати той самий результат"
print("✅ результат ідентичний — різниця лише в тому, що з цього можна прочитати")
циклом    : [68.4, 71.64, 76.8, 475.2] — рядків коду: 6
включенням: [68.4, 71.64, 76.8, 475.2] — рядків коду: 1
✅ результат ідентичний — різниця лише в тому, що з цього можна прочитати

Найкращий компроміс — не «все в один рядок» і не «все циклом», а проміжна змінна. Розбиваємо одне складне включення на два простих, кожне з яких має власне імʼя.

In [24]:
# крок 1: витягли всі пари з усіх чеків — одна проста думка
усі_пари = [пара for чек in чеки for пара in чек]

# крок 2: відібрали потрібні — друга проста думка
потрібні = [(ціна, скільки) for ціна, скільки in усі_пари if ціна < 100 and скільки > 1]

# крок 3: порахували — третя проста думка
з_націнкою = [round(ціна * скільки * 1.2, 2) for ціна, скільки in потрібні]

print("усіх пар    :", len(усі_пари))
print("потрібних   :", потрібні)
print("з націнкою  :", з_націнкою)

assert з_націнкою == монстр
print("✅ три читабельних включення = один монстр, і кожне має імʼя, що пояснює його сенс")
усіх пар    : 6
потрібних   : [(28.5, 2), (19.9, 3), (32.0, 2), (99.0, 4)]
з націнкою  : [68.4, 71.64, 76.8, 475.2]
✅ три читабельних включення = один монстр, і кожне має імʼя, що пояснює його сенс

13 · Чому включення трохи швидше¶

Включення справді швидше за цикл із append, але йдеться про десятки відсотків, а не про рази. Причина конкретна: у циклі Python на кожному елементі шукає атрибут append і викликає його як звичайну функцію, а у включенні для цього є окрема інструкція LIST_APPEND — без пошуку імені й без виклику.

Заміряємо самі. Числа на твоїй машині будуть іншими, а співвідношення — приблизно таким же.

In [25]:
import timeit

підготовка = "дані = list(range(50_000))"
час_включення = timeit.timeit("[x * 2 for x in дані]", setup=підготовка, number=100)
час_циклу = timeit.timeit(
    "результат = []\nfor x in дані:\n    результат.append(x * 2)",
    setup=підготовка, number=100)

print("включення:", round(час_включення, 3), "с на 100 повторень")
print("цикл     :", round(час_циклу, 3), "с на 100 повторень")
print("включення швидше приблизно в", round(час_циклу / час_включення, 2), "раза")
print()
print("висновок: різниця є, але вона НЕ причина переписувати цикли —")
print("читабельність важливіша за десятки відсотків на коротких списках")
включення: 0.274 с на 100 повторень
цикл     : 0.315 с на 100 повторень
включення швидше приблизно в 1.15 раза

висновок: різниця є, але вона НЕ причина переписувати цикли —
читабельність важливіша за десятки відсотків на коротких списках

14 · Круглі дужки: генераторний вираз¶

Заміни квадратні дужки на круглі — і замість списку отримаєш генераторний вираз. Він нічого не будує одразу, а віддає значення по одному на вимогу — рівно як range із теми 12. Повна історія буде в темі 30, а зараз подивимось на витрати памʼяті.

In [26]:
import sys

список = [x * 2 for x in range(1_000_000)]
генератор = (x * 2 for x in range(1_000_000))

print("тип із квадратними дужками:", type(список).__name__)
print("тип із круглими дужками   :", type(генератор).__name__)
print("список   :", sys.getsizeof(список), "байтів")
print("генератор:", sys.getsizeof(генератор), "байтів")
print()
# найчастіше генератор пишуть просто всередині виклику функції — і дужки не дублюють
разом = sum(ціна * скільки for ціна, скільки in zip(цінник, кількості))
print("сума без проміжного списку:", round(разом, 2))
assert round(разом, 2) == round(sum([ц * к for ц, к in zip(цінник, кількості)]), 2)
print("✅ результат той самий, а проміжного списку не існувало")
тип із квадратними дужками: list
тип із круглими дужками   : generator
список   : 8448728 байтів
генератор: 200 байтів

сума без проміжного списку: 305.7
✅ результат той самий, а проміжного списку не існувало

Що далі — три рівні¶

🟢 Рівень 1¶

Візьми свій список покупок (щонайменше шість позицій) і словник цін до нього. Одним включенням побудуй список назв дорожчих за середню ціну, другим — словник «назва → ціна з націнкою 15%». Зроблено, якщо обидва результати проходять assert проти написаних тобою ж циклів, а перше включення на порожньому списку дає [] без помилки.

🟡 Рівень 2¶

Дано список слів. Одним включенням побудуй словник «слово → його довжина», другим — множину різних довжин. Поясни письмово, чому в словнику може виявитись менше записів, ніж слів у списку. Зроблено, якщо виконується assert set(словник.values()) == множина_довжин і ти навів приклад списку, на якому довжина словника менша за довжину списку.

🔴 Рівень 3¶

Візьми монстра з розділу 12 і напиши три різні коректні версії того самого обчислення: (1) одним включенням, (2) звичайними циклами, (3) двома-трьома простими включеннями з проміжними змінними. Заміряй timeit-ом усі три. Зроблено, якщо всі три дають однаковий результат за assert, ти навів довжину рядка кожної версії в символах і письмово обґрунтував, яку з них узяв би в реальний проєкт — і чому.