У темі 05 ми рахували, у що обходиться
+= над рядками, і чесно писали: «циклів ми ще не вчили, це тема 12».
У темі 06 правильний рецепт таблиці
[[0] * 3 for _ in range(3)] лишився замовлянням, яке треба було просто
запамʼятати. У темі 08 біля
.items() стояла врізка: «найчастіше це використовують у циклі —
обходити ти навчишся за чотири теми». Чотири теми минуло.
Ця лекція нічого нового про структури даних не розповідає. Вона знімає обмеження: усе, що ми складали в списки, кортежі, словники й множини, нарешті можна пройти елемент за елементом і щось із кожним зробити. Наскрізний приклад лишається той самий, що й у темі 06, — кошик покупок:
01 / ОбхідГоловна відмінність від інших мов
У багатьох мовах цикл — це лічильник. Ти заводиш змінну, ставиш їй початкове значення, пишеш умову зупинки, пишеш, як її збільшувати, а потім всередині вручну дістаєш елемент за номером. Приблизно так це виглядає в C або Java:
У Python for влаштований інакше, і це не косметична різниця. Він
не рахує. Він просить у колекції наступний елемент — і кладе його
в імʼя:
Читається буквально як речення: «для кожної назви в покупках — надрукуй назву».
Ніякого i, ніякої довжини, ніякого покупки[i]. Розберімо
рядок по частинах:
for— ключове слово. Далі йде імʼя змінної циклу: ти вигадуєш його сам, і Python на кожному кроці перечіплює його на черговий елемент.in— той самий оператор, що й у перевірці"мед" in покупки, але тут він частина синтаксисуfor, а не питання. Після нього стоїть те, що обходимо.- Двокрапка в кінці — обовʼязкова. Її забувають частіше за все інше.
- Відступ у наступних рядках — це і є тіло циклу. Рівно як в умовах з теми 11: не дужки, а чотири пробіли ліворуч визначають, що саме повторюється.
Тіло виконується один раз на кожен елемент. Коли елементи скінчились, цикл завершується сам — писати умову зупинки не треба, бо колекція сама знає, де в неї кінець. Перший рядок без відступу після циклу виконається вже після всіх повторень.
Найкорисніше, що можна зробити в циклі, — накопичувати. Заводимо змінну до циклу й на кожному кроці її доповнюємо:
Змінна разом оголошена зовні, і це принципово: якби
рядок разом = 0 стояв усередині з відступом, він виконувався б на кожному
кроці й обнуляв би все зроблене. Пройди цей код покроково — і перемкни колекцію,
щоб побачити, що механіка не залежить від того, що ми обходимо.
Інтерактив 1 · Крок за кроком
Підсвічений рядок — той, що виконується зараз. Праворуч видно, як змінюються імена після кожного кроку.
назва і ціна. Це те саме
розпакування кортежу, що й у темі 07:
.items() віддає пари, а for одразу їх розкладає.02 / ОбіцянкиЩо саме можна обходити
Тепер найприємніше. Оскільки for нічого не рахує, а просить наступний
елемент, йому байдуже, у кого просити. Обходиться будь-що, що вміє віддавати елементи
по черзі, — такі обʼєкти називають ітерованими (iterable). Пройдімось
по всьому, що ми вже вивчили.
Список — елементи в порядку зберігання
Порядок гарантований: список упорядкований, і обхід іде зліва направо.
Рядок — по одному символу
Це та сама послідовність, що й у темі 05, тільки тепер ми йдемо по ній замість
брати зріз. Тут же ховається класична помилка: for слово in "мед і хліб"
дасть символи, а не слова. Щоб отримати слова, спершу поділи рядок:
for слово in "мед і хліб".split().
Множина — без порядку
Обхід працює, але порядок не визначений — множина з
теми 09 зберігає елементи так, як їй зручно
за хешем. Покладатись на порядок виводу тут не можна. Потрібен передбачуваний
порядок — обходь sorted(знижки).
Словник — і три його вікна
Ось обіцянка з теми 08, яку ми нарешті виконуємо. Сам словник обходиться по ключах:
Але майже завжди тобі потрібне й значення. Тут і стають у пригоді три методи-вікна, з якими ми познайомились, але не могли скористатись:
| як обходимо | що дає змінна циклу | коли брати |
|---|---|---|
| for k in ціни | ключ | значення не потрібне |
| for v in ціни.values() | значення | ключ не потрібен: сума, максимум |
| for k, v in ціни.items() | ключ і значення одразу | найчастіший випадок |
| for k in sorted(ціни) | ключ, в упорядкованому вигляді | треба стабільний вивід |
Форма for назва, ціна in ... — це не особлива магія словника, а
розпакування з теми 07. .items() віддає пари
('хліб', 28.5), а for одразу розкладає кожну пару на два
імені. Так само працює обхід будь-якого списку кортежів.
03 / rangeКоли потрібні саме числа
А що, як обходити треба не колекцію, а просто «п'ять разів»? Для цього є
range — обʼєкт, який видає цілі числа за арифметичним правилом:
Три форми виклику, і всі три варто знати напамʼять:
range(стоп)— від нуля достоп, не включаючи його.range(5)— це 0, 1, 2, 3, 4: п'ять чисел, останнє на одиницю менше за п'ять.range(старт, стоп)— відстартвключно достопневключно.range(старт, стоп, крок)— те саме, але додаємокрок. Крок може бути відʼємним — тоді йдемо вниз.
Правило «стоп не входить» здається примхою, поки не помітиш, що воно робить довжину
очевидною: у range(0, 5) рівно 5 значень, а range(a, b) має
рівно b − a значень. Загальна формула кількості — з округленням угору:
Підстав range(2, 12, 3): (12 − 2) / 3 = 3.33…, угору — 4.
І справді 2, 5, 8, 11 — чотири числа, останнє менше за 12. Покрути повзунки й перевір
формулу на будь-яких значеннях, включно з відʼємним кроком і порожнім результатом:
Інтерактив 2 · range наочно
Числова вісь. Рожеві кружечки — те, що range справді
видасть; дуги показують крок.
ValueError, і слушно: значення ніколи не зрушило б з місця.
Старт праворуч від стопу при додатному кроці дає порожній range —
не помилку, а нуль повторень. І відʼємний крок працює дзеркально: тоді порожнім
буде вже випадок «старт лівіше за стоп».Тепер найважливіше про range, і це те, що майже ніколи не пояснюють
новачкам. range — не список. Він не зберігає числа,
а обчислює чергове за формулою старт + i · крок у момент, коли його
попросили. Перевір напряму:
Мільйон чисел у range займає 48 байтів — стільки ж, скільки й пʼять
чисел. Він тримає лише старт, стоп і крок. Той самий мільйон у списку — вісім мегабайтів.
Тому for i in range(1_000_000) запускається миттєво й не їсть памʼять,
а for i in list(range(1_000_000)) спершу чесно збудує весь список.
range без потреби. Якщо
в тебе вже є колекція — обходь її, а не її індекси. Запис
for i in range(len(покупки)) з наступним покупки[i] — майже
завжди ознака того, що людина писала цикл за звичкою з іншої мови. Він довший,
повільніший і ламається, щойно колекція виявиться множиною або словником, де індексів
немає взагалі. До цього ми ще повернемось у розділі 10.04 / Париenumerate і zip
Іноді індекс усе-таки потрібен — наприклад, щоб надрукувати пронумерований чек.
Для цього є enumerate: він бере ітерований обʼєкт і віддає
пари «номер, елемент».
Нумерація починається з нуля, як усюди в Python. Але людям звичніше з одиниці —
тому в enumerate є другий аргумент: enumerate(покупки, start=1)
дасть 1, 2, 3, 4, 5. Важливо: start зсуває лише лічильник, а не
початок обходу. Перший елемент усе одно буде «хліб», просто з номером 1.
Друга ситуація — треба йти двома наборами одночасно. Назви лежать
в одному списку, кількості в другому, і потрібні вони парами. Для цього є
zip — назва від застібки-блискавки, яка зчіплює два ряди зубців:
І тут ховається поведінка, яку треба знати заздалегідь, бо мовчки вона нічого не
скаже: zip зупиняється на найкоротшому наборі. Якщо
кількостей чотири, а покупок пʼять, ти отримаєш чотири пари — і жодного попередження
про те, що «сіль» лишилась без пари. Подивись, як утворюються пари в обох випадках:
Інтерактив 3 · enumerate і zip
Ліворуч — джерела, праворуч — пари, які отримає цикл на кожному кроці.
zip мовчки обрізає обхід по коротшому: пʼятий елемент просто ніколи
не потрапить у цикл. Якщо така розбіжність — це помилка в даних, у Python 3.10+
є zip(a, b, strict=True): він кине ValueError замість
того, щоб мовчати.05 / Керуванняbreak і continue
Іноді проходити всю колекцію до кінця не треба. Два ключові слова змінюють хід циклу, і плутають їх постійно:
break— вийти з циклу зовсім. Решта елементів не буде розглянута ніколи. Виконання продовжиться з першого рядка після циклу.continue— перейти до наступного елемента. Решта тіла на цьому кроці пропускається, але сам цикл живий і йде далі.
Найкраще різниця видно, коли той самий цикл виконати тричі, міняючи лише одне слово. Рахуємо суму цін, але дорогі позиції (понад 100 грн) хочемо обійти:
Слово pass — це офіційна заглушка «нічого не роби»: Python вимагає, щоб
у тілі if був хоч один рядок, і pass цю вимогу задовольняє,
не роблячи нічого. Тепер підстав по черзі всі три варіанти:
Інтерактив 4 · break проти continue
Один і той самий цикл, змінюється лише одне слово в тілі.
pass
(додались усі пʼять), 92.4 з continue (мед пропущено, сіль додано)
і 80.4 з break (на меді вийшли, солі навіть не бачили). Різниця між
двома останніми — рівно одна позиція на 12 грн, і саме на такому пропущеному
«хвості» ловляться справжні баги.while. Це не властивість
for, а керування будь-яким циклом. І обидва діють лише на
найближчий цикл: із вкладеного break вискочить у
зовнішній, а не з обох одразу. Про вкладені цикли — розділ 08.06 / elseГілка, яку майже всі розуміють неправильно
У Python у циклу може бути else. Конструкція рідкісна, і майже кожен,
хто бачить її вперше, читає її неправильно — як «якщо цикл не виконався» або «якщо
колекція порожня». Обидва прочитання хибні.
Правило одне, і воно просте: else виконується тоді, коли цикл
завершився природно — тобто без break. Якщо
break спрацював, гілка else пропускається.
Через це багато хто пропонує читати це слово не як «інакше», а як «якщо не перервали» — або, ще точніше, «nobreak». Класичне застосування — пошук:
Без else довелось би заводити окрему змінну-прапорець
(знайдено = False, потім знайдено = True перед
break, потім перевірка після циклу). Гілка else робить те
саме без зайвої змінної. Зверни увагу на відступ: else вирівняно з
for, а не з if — це else саме циклу.
Інтерактив 5 · Коли спрацьовує else у циклі
Три сценарії того самого пошуку. Внизу — дві гілки, підсвічена та, що виконалась.
else усе одно
спрацював. Бо умова не «чи були кроки», а «чи був break». Не було —
отже, гілка виконується.07 / whileКоли кількість кроків невідома
for добрий, коли є що обходити. Але буває інакше: кроків стільки,
скільки треба, і скільки саме — заздалегідь невідомо. Тоді береться
while — «поки». Він повторює тіло, доки умова
залишається істинною:
Умова перевіряється перед кожним повторенням, у тому числі перед
найпершим. Якщо вона хибна одразу, тіло не виконається жодного разу. Правила
істинності — ті самі, що в темі 11:
порожній список у while хибний, непорожній — істинний.
І тут зʼявляється єдина справжня небезпека циклів. Умова сама себе не змінить.
Якщо в тілі немає нічого, що рухає її до хибності, — цикл не завершиться
ніколи. Програма зависне, вентилятор загуде, і зупинити її доведеться
через Ctrl+C. Постав крок у нуль і подивись, що станеться:
Інтерактив 6 · while і нескінченний цикл
Скільки списуємо з гаманця за один крок. Лінія показує, що
відбувається з умовою гаманець > 0.
гаманець -= 0 виконується справно, але нічого не змінює —
умова застигла істинною. Відʼємний крок гірший: гаманець росте, тобто
кожне повторення віддаляє нас від виходу. Обидва випадки виглядають однаково
ззовні — програма просто не відповідає.Профілактика проста і складається з трьох звичок:
- Перед тим як писати
while, скажи вголос, що саме в тілі наближає умову до хибності. Не можеш назвати рядок — цикл нескінченний. - Перевір крайні значення. Нуль, відʼємне, порожній список — саме там ламається.
- Обмеж кількість спроб, якщо умова залежить від зовнішнього
світу:
while спроб < 5 and not вдалось:.
Окремо варто знати цілком легальну ідіому while True: — нескінченний
цикл навмисне, з виходом через break усередині. Так пишуть, коли
умову зручніше перевіряти не на початку, а посередині тіла: наприклад, спершу спитати
щось у користувача, а потім вирішити, чи виходити. Різниця з випадковим зациклюванням
одна: тут вихід є, і він видимий у коді.
for, а коли while.
Питай себе: «чи знаю я наперед, що саме обходжу?» Є колекція, є діапазон чисел —
for, майже завжди. Кроки залежать від того, що станеться всередині
(поки не збіжиться, поки користувач не введе правильно, поки в гаманці є гроші) —
while. Спроба зробити на while те, що просилось на
for, майже завжди дає довший код із зайвим лічильником.08 / ВкладеніЦикл усередині циклу
Тіло циклу — звичайний код, отже, в ньому може стояти інший цикл. Внутрішній проходить повністю на кожен крок зовнішнього. Якщо зовнішній робить 3 кроки, а внутрішній 4, тіло внутрішнього виконається 3 × 4 = 12 разів.
Класична демонстрація — таблиця множення:
Тут добре видно розподіл ролей: внутрішній print має відступ
у вісім пробілів і належить внутрішньому циклу — він друкує клітинки. А
print() з відступом у чотири пробіли належить зовнішньому
циклу: він виконується один раз на рядок і переносить каретку. Помилка на один рівень
відступу тут одразу видно в результаті — і це найкращий спосіб відчути, що відступ
у Python не оформлення, а синтаксис.
У нашому кошику вкладені цикли дадуть прайс «скільки коштує k штук»:
Про вкладені цикли треба знати одну неприємну річ: вони множать роботу. Два вкладені цикли по n кроків — це n² повторень тіла. На n = 100 це 10 тисяч, ще терпимо; на n = 10 000 це вже сто мільйонів, і програма думатиме хвилини. Коли бачиш вкладений цикл по тих самих даних — варто спитати, чи не можна замінити внутрішній пошук на множину або словник, де перевірка коштує O(1). Це рівно та розмова, яку ми вели в темі 10.
09 / ПасткаЗміна колекції під час обходу
А тепер найдорожча помилка цієї теми. Виглядає вона абсолютно природно: обходимо список і викидаємо з нього те, що не потрібне.
Помилки немає, програма не впала, результат неправильний. Це найгірший вид бага — той, що мовчить.
Причина в тому, як for ходить по списку. Усередині він тримає
прихований лічильник позиції: віддав елемент 0, збільшив на одиницю,
віддав елемент 1, збільшив… Коли ти видаляєш елемент, усі, хто стояв правіше,
зсуваються вліво на одну комірку — а лічильник про це не знає й
однаково йде вперед. Один елемент проскакує повз. Пройди покроково й подивись на
прихований індекс:
Інтерактив 7 · Чому елементи «перескакуються»
Угорі — неправильний спосіб, унизу — правильний. Помаранчева комірка та, на яку зараз дивиться прихований лічильник.
покупки[:]: копія не зсувається, бо змінюємо ми не її.Правильних способів три, і всі три варто знати:
Перший — найкоротший і найчастіший: квадратні дужки [:] роблять
поверхневу копію (тема 06), цикл іде по ній, а remove псує оригінал —
і ці двоє більше не заважають одне одному. Другий — найчесніший: нічого не ламаємо,
а будуємо новий список. Саме він у темі 13
перетвориться на один рядок.
RuntimeError: dictionary changed size during iteration. Виглядає
суворіше, але насправді це подарунок: помилка краща за тихо зіпсовані дані. Ліки ті
самі — обходь копію: for назва in list(ціни):.10 / УсерединіІтератор, і чому він одноразовий
Розділ для тих, кому мало «працює — і добре». Ми весь час казали «for
просить наступний елемент». У кого саме він просить?
Насправді за цим стоять два різні поняття. Ітерований обʼєкт
(iterable) — це той, у кого можна попросити ітератор: список, рядок, словник,
множина, range. Ітератор (iterator) — маленький обʼєкт
із єдиним умінням: віддати наступний елемент і запамʼятати, де він зупинився.
Цикл for — це синтаксичний цукор над трьома діями:
Функція iter() питає в колекції обʼєкт-обхідник. Для списку це
list_iterator, який усередині тримає лише посилання на список і
число — поточну позицію.
Функція next() просить наступний елемент і зсуває позицію.
Це буквально те, що робить for на кожному повторенні.
Коли елементи скінчились, ітератор кидає особливий виняток
StopIteration. Це не помилка, а сигнал: цикл
for ловить його й тихо завершується. Побачити його «голим» можна,
лише викликавши next() вручну.
З цієї механіки випливають три речі, які пояснюють раніші дивацтва.
Ітератор одноразовий. Він не вміє повертатись на початок —
позиція тільки росте. Це не видно на списку (бо for щоразу просить
новий ітератор), але одразу видно на zip чи
enumerate, які самі є ітераторами:
Другий цикл не впаде з помилкою — він просто не зробить жодного кроку, бо перший
next() одразу дасть StopIteration. Це одна з найпідступніших
несподіванок Python. Ліки: якщо результат потрібен двічі, зафіксуй його списком —
пари = list(zip(покупки, кількості)).
Прихований лічильник — це поле ітератора. Ось звідки береться
пастка з розділу 09: list_iterator тримає позицію, а не копію списку.
Видалив елемент — список зсунувся, позиція лишилась. Тепер зрозуміло, чому обхід
копії лікує: у копії ніхто нічого не зсуває.
І звідси ж — вирок конструкції for i in range(len(покупки)).
Вона працює, але робить зайве коло: замість того щоб узяти елемент, який ітератор
і так має віддати, ти береш число, а потім ідеш по нього в список ще раз. Порівняй
три записи однієї задачі:
| запис | що не так |
|---|---|
| for i in range(len(покупки)): print(покупки[i]) |
звичка з інших мов: зайвий доступ за індексом, не працює для множини й словника |
| for i in range(len(покупки)): print(i, покупки[i]) |
уже краще, але це рівно те, що робить enumerate — і коротше |
| for i, назва in enumerate(покупки): print(i, назва) |
так і треба |
Єдиний випадок, де range(len(...)) справді доречний, — коли індекс
потрібен не для читання, а для запису в той самий список:
покупки[i] = покупки[i].upper(). Ітератор віддає значення, а не комірку,
тож присвоїти через змінну циклу неможливо — і тоді номер комірки справді потрібен.
11 / ПідсумокЩо забрати з теми
Одна конструкція, яка знімає обмеження з усього попереднього блоку:
forобходить, а не рахує. Він просить наступний елемент, тому працює однаково зі списком, кортежем, рядком, множиною й словником. Індекси при цьому не потрібні.- Словник обходиться трьома вікнами. Сам по собі — по ключах,
.values()— по значеннях,.items()— парами, якіforодразу розпаковує у дві змінні. range— не список. Він тримає три числа й обчислює решту на льоту; мільйон значень коштує 48 байтів. Стоп ніколи не входить.enumerateдає номер,zipзчіплює набори — і мовчки зупиняється на найкоротшому, якщо не поставитиstrict=True.breakвиходить із циклу,continue— лише з поточного кроку. Обидва діють на найближчий цикл, не на всі.elseу циклі — це «якщо не булоbreak». На порожній колекції він теж спрацює: кроків не було, перерви теж не було.while— коли кроків невідомо. Умова сама себе не змінить: якщо в тілі немає рядка, що веде її до хибності, цикл нескінченний.- Не змінюй колекцію, поки її обходиш. Список тихо перескочить
елемент, словник кине
RuntimeError. Обходь копію (покупки[:],list(ціни)) або збирай новий набір.
У темі 13 ми
візьмемо найчастіший цикл із усіх — той, що збирає новий список з наявного, — і
запишемо його одним рядком. Це спискове включення, і саме звідти
беруться ті [[0] * 3 for _ in range(3)], які ми двічі просили просто
запамʼятати. Тепер у тебе є все, щоб зрозуміти, чому цей рядок означає рівно те,
що означає.
practice.ipynb ти напишеш
цикли підрахунку, пошуку й накопичення, обійдеш словник трьома способами, збудуєш
таблицю множення вкладеними циклами — і власноруч відтвориш пастку зі зміною списку,
щоб побачити неправильну відповідь на власному екрані. Кожен висновок перевіримо
через assert.Далі в темі
Теорію прочитано. Тепер закріпи її на практиці.