У темі 12, коли ми розбирали
пастку зі зміною списку під час обходу, серед трьох правильних рецептів був такий:
«збираємо новий список замість того, щоб псувати старий». І поруч стояло обіцяння:
саме він у темі 13 перетвориться на один рядок. Ось той рядок. А ще звідси
нарешті стане зрозумілим замовляння [[0] * 3 for _ in range(3)], яке ми
двічі просили просто запамʼятати.
Наскрізний приклад лишається тим самим кошиком покупок, що й у двох попередніх темах:
01 / МотивНайчастіший цикл із усіх
Придивись до циклів, які ти вже писав, і ти помітиш, що страшенно багато з них влаштовані однаково. Заводимо порожній список. Обходимо щось. На кожному кроці щось рахуємо й додаємо в кінець. Друкуємо результат.
Три рядки, з яких справжня думка тільки в одному — у тому, де
ціна * 2. Решта — обслуговування: порожній список, який треба не забути
створити, і .append(), який щоразу однаковий. Це помітно не лише тобі:
такий цикл трапляється в коді так часто, що для нього зробили окрему форму запису.
Це спискове включення (list comprehension). Результат буквально той самий список, до останнього байта — це не «майже те саме» й не «швидший варіант для простих випадків». Той самий.
Найважливіше тут — не економія двох рядків. Важливо, що саме ти описуєш. Цикл описує процес: створи ящик, візьми елемент, поклади в ящик, візьми наступний. Включення описує результат: «список подвоєних цін для кожної ціни з цінника». Перше — інструкція складальнику. Друге — визначення множини, майже як у математиці, де ми писали б {2c : c ∈ цінник}.
Через це різниця й у тому, як ти читаєш чужий код. Побачивши цикл, ти мусиш
виконати його в голові, щоб зрозуміти, що вийде. Побачивши включення, ти читаєш
відповідь одразу — вона стоїть першою, ще до слова for. Перемкни задачі
в інтерактиві й подивись, яка частина циклу куди переїхала:
Інтерактив 1 · Цикл ↔ включення
Той самий алгоритм двома записами. Стрілки показують, що куди переїхало; кольори однакові в обох варіантах.
ціна * 2 стоїть
останнім — він у тілі, після всього. У включенні той самий вираз стоїть
першим. Це і є та перестановка, через яку включення читається як
відповідь: спершу «що вийде», а вже потім «з чого».02 / АнатоміяЧотири частини й порядок читання
Розберімо запис на складники. Частин рівно чотири, і третя з них необовʼязкова:
- Вираз — те, що потрапить у новий список на кожному кроці.
Це може бути сама змінна (
ціна), арифметика (ціна * 2), виклик методу (назва.upper()) — будь-що, що дає значення. for імʼя— та сама змінна циклу, що й у темі 12. Ти вигадуєш імʼя сам, і воно існує лише всередині включення.in джерело— будь-що ітероване: список, кортеж, рядок, множина, словник,range,zip(...). Правила точнісінько ті самі, що для звичайногоfor.if умова— фільтр. Якщо його немає, беруться всі елементи; якщо є, у результат потрапляють лише ті, для яких умова істинна.
А тепер про порядок, який спершу збиває з пантелику. Пишемо ми зліва направо, а виконується це справа наліво: спершу Python бере джерело, потім відсіює за умовою, і лише для тих, хто пройшов, обчислює вираз. Тобто прочитати включення вголос правильно так:
Це дзеркально до того, як воно записане. Незручно? Так. Але саме тому вираз стоїть
попереду: у 90% випадків тебе цікавить що вийде, а не звідки.
Погратись зі складниками найпростіше на конструкторі — збери свій вираз і подивись,
що вийде. Зверни увагу на випадок «без умови»: if справді можна не писати
взагалі, і це найчастіша форма.
Інтерактив 2 · Конструктор включення
Обери джерело, перетворення й умову — вираз збереться сам, а знизу видно, що він поверне.
джерело
вираз
умова
03 / Фільтрif у кінці: список коротшає
Умова в кінці включення робить рівно одне: вирішує, чи потрапить елемент
у результат. Це прямий аналог continue зі звичайного циклу.
Тут добре видно, чому вираз і змінна циклу часто збігаються: ми нічого не
перетворюємо, ми відбираємо. Запис [ціна for ціна in цінник if …]
виглядає надлишковим, але він чесний — вираз є завжди, просто цього разу він
тривіальний.
Умова може бути будь-якою — усе, що ми вчили в темі 11, працює тут без змін:
І тут же — головна плутанина цієї теми, через яку люди годинами шукають, «чому воно не працює».
04 / ТернарПеретворення: умова на початку
Уяви іншу задачу. Не «залиш дешеві», а «обріж дорогі до 100» — тобто ціни понад сотню замінити на сотню, а решту лишити як є. Кількість позицій має лишитись тією самою: ми нічого не викидаємо з кошика.
Спроба написати це через if у кінці приречена: там if
взагалі не вміє нічого замінювати, він лише впускає або не впускає. Потрібен
тернарний вираз — той самий a if умова else b з
теми 11. І стоїть він
на місці виразу, тобто на початку:
Два записи виглядають майже однаково, а роблять принципово різне:
| запис | де стоїть if | що робить | довжина результату |
|---|---|---|---|
| [x for x in дані if умова] | у кінці | фільтрує: пропускає або ні | менша або рівна |
| [a if умова else b for x in дані] | на початку | перетворює: обирає значення | завжди та сама |
Запамʼятати легко за одним правилом: else буває тільки в
тернарі. Якщо в тебе є else — він мусить стояти перед
for. Якщо else немає — if іде після
for. Спроба поставити фільтр із else в кінець дає
SyntaxError ще до запуску:
Повідомлення на перший погляд дивне («очікував else після if» — а він же й
стоїть!), але воно правильне: Python побачив if і спробував прочитати
його як початок тернарного виразу, а тернар не може стояти в хвості. Посунь повзунок
і подивись на обидва варіанти одночасно — на тих самих даних і з тією самою межею:
Інтерактив 3 · Фільтр проти перетворення
Одна межа, одні дані, два записи. Угорі список коротшає, унизу — ні.
[ц if ц < 100 else 100 for ц in цінник if ц > 15] цілком законний:
перший if — тернар у виразі, другий — фільтр. Він і фільтрує, і
перетворює. Але прочитати його з першого разу вже важко — і це чудовий привід
перейти до розділу 07.05 / ДужкиТри дужки — три типи
Включення — це не «щось про списки». Це форма запису, і те, який тип вийде, визначають дужки та наявність двокрапки у виразі. Три варіанти:
Правило просте:
- Квадратні дужки — список. Порядок зберігається, дублікати зберігаються.
- Фігурні дужки з одним виразом — множина (тема 09). Дублікати злипаються, порядок не гарантований.
- Фігурні дужки з
ключ: значення— словник (тема 08). Двокрапка у виразі — це вся різниця між множинним і словниковим включенням.
Найкраще різницю видно на даних із повторами. Візьмімо список, у якому «мед» і
«хліб» трапляються двічі, і застосуймо до нього той самий вираз
len(н), міняючи лише дужки:
Інтерактив 4 · Три дужки — три типи
Джерело й вираз незмінні. Змінюються тільки дужки — і разом із ними тип результату.
Словникове включення особливо корисне в парі зі zip — так із двох
паралельних списків народжується словник в один рядок:
Тут працює розпакування з теми 07: після
for стоять два імені, бо zip віддає пари.
Так само можна перевернути наявний словник — поміняти ключі й значення місцями:
{} означає порожній словник, а не множину,
бо словники зʼявилися в мові раніше. Порожня множина — тільки set().
На включення це не впливає (там дужки ніколи не порожні), але поруч плутають
постійно.06 / ВкладеніДва for в одному включенні
У включенні може бути більше одного for. Це рівно те саме, що
вкладені цикли з теми 12, і порядок
for-ів такий самий, як у циклах: зліва направо = зовні
всередину.
Класична задача — «розплющити» (flatten) список списків у один плаский список:
Поклади ці два записи поруч і прочитай зверху вниз проти зліва направо — вони
збігаються слово в слово: for рядок in матриця, потім
for значення in рядок, потім вираз. Різниця лише в тому, що у включенні
вираз переїхав уперед.
Звідси випливає й правило, чому переставити for-и не можна. Другий
for бере джерело з першого: рядок існує лише тому, що його
щойно створив зовнішній for. Якщо поміняти їх місцями, ім'я
рядок згадається раніше, ніж зʼявиться, — і ти отримаєш
NameError. Перевір обидва режими:
Інтерактив 5 · Вкладене включення
Крок за кроком: який for зараз працює і в якому
порядку заповнюється результат.
for
робить три кроки, внутрішній на кожному з них — чотири; разом 3 × 4 = 12
значень, і всі вони лягають у результат у тому порядку, в якому їх зустріли.
У другому режимі коду взагалі не існує: рядок згадано раніше, ніж
створено.Два for — це не те саме, що вкладене включення
Є ще одна форма, яку легко переплутати з попередньою. Якщо треба не розплющити матрицю, а побудувати її, включення ставлять усередину виразу:
Ось воно — те замовляння з теми 06. Тепер
його можна прочитати: «для кожного з трьох кроків поклади в список новий рядок
[0] * 3». Ключове слово — новий: вираз обчислюється
заново на кожному кроці, тому три рядки — три різні списки. А в поламаному варіанті
[[0] * 3] * 3 множення копіює посилання на один-єдиний список,
і зміна в одному рядку відображається в усіх трьох. Саме тому там і потрібне
включення.
Підкреслення _ — це звичайне імʼя змінної, не синтаксис. Домовленість
така: якщо значення нам не потрібне, а потрібна лише кількість повторень, називаємо
змінну _, щоб читач одразу бачив: «сюди не дивись, воно не
використовується».
07 / СмакКоли включення шкодить
Головний розділ теми — і єдиний, де немає правильної відповіді у вигляді синтаксису. Включення — інструмент читабельності. Щойно воно перестає бути читабельним, воно втрачає весь сенс і лишає тільки шкоду.
Правило, яке працює найкраще, звучить так: якщо включення не читається вголос за один подих — пиши цикл. Не «за один рядок», не «за 79 символів», а саме вголос і за один подих, бо це перевіряє те, що справді важливо: чи тримає читач у голові всю конструкцію одразу.
Три ознаки того, що межу перейдено:
- Довжина. Один рядок, що не влазить у ширину екрана й переноситься редактором, уже не «компактний запис».
- Вкладеність. Два
forще терпимо, три — майже ніколи. Включення всередині виразу іншого включення — окремий біль: зовнішнє й внутрішнє читаються в різних напрямках. - Кількість рухомих частин. Порахуй разом:
for-и,if-и, виклики функцій і розпакування в змінній циклу. До трьох — нормально; чотири й більше — цикл буде коротшим для розуміння, навіть якщо довшим у рядках.
Посунь повзунок складності й подивись, як той самий тип виразу з кожним кроком втрачає читабельність — а поруч стоїть чесний цикл, який усі ці кроки витримує без жодних зусиль:
Інтерактив 6 · Межа читабельності
Один повзунок нарощує складність. Нижче — той самий код звичайним циклом; його довжина росте набагато повільніше за складність.
дорогі = [ц for ц in цінник
if ц > 100], потім [round(ц * 1.2, 2) for ц in дорогі]. Два
простих включення читаються легше за одне складне — і кожне з них має імʼя, яке
пояснює, що це.08 / Побічний ефектВключення, результат якого нікому не потрібен
Типова помилка новачка, який щойно вивчив включення й хоче застосовувати його всюди:
Надрукувалось! Виглядає як робочий код. І він справді працює — але робить не те,
що написано. Написано «збери список із результатів print(назва)».
Функція print нічого не повертає, тобто повертає None, —
і Python слухняно збирає список із пʼяти None, після чого викидає його
геть, бо результат нікуди не присвоїли:
Ми виділили памʼять під список, заповнили його порожнечею й одразу забули. На
пʼятьох покупках це не помітно. На мільйоні рядків з файлу — це мільйон
None у памʼяті, які нікому не потрібні.
Але головна шкода не в памʼяті, а в брехні. Дужки […] обіцяють
читачеві, що тут будують список. Читач шукає, куди цей список подівся, — і не
знаходить. Правило просте й категоричне:
for. Він рівно для цього й існує. Ознака помилки
проста: включення, яке не стоїть праворуч від = і не передається
у функцію, майже завжди зайве.09 / УсерединіОбласть видимості й швидкість
Розділ для тих, кому мало «працює — і добре». Три речі, які пояснюють, чому включення поводиться саме так.
Змінна циклу не тече назовні
У звичайному циклі змінна після завершення лишається живою — ми бачили це в темі 12. У включенні — ні:
Включення має власну область видимості: ім'я ч
живе тільки всередині квадратних дужок і зникає разом із ними. Це рятує від
класичної біди — випадкового затирання змінної, яка вже була в тебе під тим самим
іменем.
У Python 2 було інакше: включення виконувалось у тій самій області, що й навколишній код, і змінна циклу справді витікала назовні. Це вважалось помилкою дизайну й було виправлено в Python 3. Якщо колись зустрінеш старий код, який розраховує на витік, — знай, що на сучасному Python він мовчки поводитиметься інакше.
:= усередині
включення навмисно присвоює в зовнішню область — це його заявлена
поведінка, а не витік. Але писати такий код без крайньої потреби не варто: він
порушує саме ту гарантію ізоляції, заради якої область і робили.Чому включення трохи швидше
Включення справді швидше за еквівалентний цикл із append — але
йдеться про десятки відсотків, а не про рази. Заміряймо на списку з 200 000 чисел:
Приблизно на 20% швидше. Причина одна й дуже конкретна: у циклі на кожному
елементі Python має знайти атрибут append у списку та викликати його як
звичайну функцію. У включенні цієї роботи немає — компілятор ставить окрему
інструкцію LIST_APPEND, яка одразу кладе значення в список без пошуку
імені й без виклику.
Звідси й практичний висновок, зворотний до очікуваного: не переписуй цикли на включення заради швидкості. 20% на мільйонах елементів помітні, на сотнях — ні, а читабельність ти втратиш одразу. Швидкість тут приємний побічний ефект, а не причина.
А якщо список не потрібен зовсім?
Останнє. Заміни квадратні дужки на круглі — і отримаєш не список, а генераторний вираз (generator expression):
Він не будує нічого одразу, а віддає значення по одному на вимогу — рівно як
range з теми 12, який теж не список. Різниця у витратах драматична:
Найчастіше його пишуть просто всередині виклику функції, і тоді дужки навіть не
дублюють: sum(ціна * к for ціна, к in zip(цінник, кількості)) порахує
суму, жодного разу не створивши проміжного списку. Деталі — у
темі 30 про ітератори й генератори; поки що достатньо знати, що
кругла дужка тут не одруківка.
10 / ПідсумокЩо забрати з теми
- Включення описує результат, а не процес. Це не «коротший цикл»: вираз стоїть попереду саме тому, що читача цікавить відповідь, а не порядок дій.
- Чотири частини: вираз ·
for імʼя·in джерело·if умова. Остання необовʼязкова. Виконується справа наліво: джерело → фільтр → вираз. ifу кінці фільтрує, тернар на початку перетворює. Перше скорочує список, друге зберігає його довжину. Ознака:elseбуває тільки в тернарі, тобто тільки передfor.- Дужки визначають тип.
[…]— список,{…}— множина,{k: v …}— словник. Множина злипає дублікати, словник злипає однакові ключі. - Два
for= вкладені цикли, у тому самому порядку: зліва направо — зовні всередину. Переставити їх не можна, бо друге джерело створює першийfor. - Не читається вголос за один подих — пиши цикл. Довжина, вкладеність і кількість рухомих частин; від чотирьох частин цикл зрозуміліший.
- Включення заради побічного ефекту — помилка.
[print(x) for x in …]будує список ізNoneі викидає його. Потрібна дія — пишиfor. - Змінна включення не тече назовні — на відміну від циклу й на відміну від Python 2. Швидше за цикл приблизно на 20%, і це не причина його переписувати.
У темі 14 ми нарешті дійдемо до функцій — і
одразу побачимо, як вони змінюють усе сказане тут. Зʼявиться lambda,
зʼявляться map, filter і sorted(key=…), і
виникне питання, яке ділить спільноту досі: коли брати включення, а коли
map. Спойлер: у Python майже завжди включення — і тепер ти вже знаєш
чому.
practice.ipynb ти
перетвориш і відфільтруєш списки, збереш словник із двох списків через
zip, розплющиш матрицю, а наостанок навмисно напишеш нечитабельне
включення й перепишеш його циклом. Кожне включення ми звіримо з еквівалентним
циклом через assert — щоб побачити на власному екрані, що це справді
той самий результат.Далі в темі
Теорію прочитано. Тепер закріпи її на практиці.