Практика · Цикли¶

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

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

  1. обійдемо список і накопичимо суму — і звіримо її з вбудованою sum();
  2. порахуємо й знайдемо те, що треба, — з break і з else;
  3. обійдемо рядок, множину й словник трьома способами;
  4. подивимось, чому range не список, і зберемо чек через enumerate і zip;
  5. напишемо while і побачимо, чого саме бракує нескінченному циклу;
  6. зробимо таблицю множення вкладеними циклами;
  7. власноруч відтворимо пастку зі зміною списку під час обходу — і полагодимо її.

Кожен висновок перевіримо через assert: він мовчить, коли все правильно, і кричить, коли ні.

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

Три структури: список назв, словник цін і список кількостей. Порядок у списках узгоджений — і саме на це ми потім спиратимемось у zip.

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

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

2 · Обхід списку: for не рахує, а бере елементи¶

Зверни увагу: жодного індексу. Змінна назва на кожному кроці перечіплюється на черговий елемент.

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

# змінна циклу не зникає після циклу — вона тримає ОСТАННЄ значення
print("після циклу назва =", назва)
у кошику: хліб
у кошику: молоко
у кошику: яблука
у кошику: мед
у кошику: сіль
після циклу назва = сіль

3 · Накопичення: сума цін¶

Накопичувач створюємо до циклу. Якби разом = 0 стояв із відступом, він обнулявся б на кожному кроці.

І одразу перевіримо себе вбудованою sum() — щоб побачити, що всередині неї нема магії, а є той самий цикл.

In [3]:
разом = 0
for ціна in ціни.values():
    разом = разом + ціна          # те саме, що разом += ціна

бібліотечна_сума = sum(ціни.values())

print("наш цикл дав :", round(разом, 2))
print("sum() дала   :", round(бібліотечна_сума, 2))
assert round(разом, 2) == round(бібліотечна_сума, 2), "наш цикл розійшовся з sum()"
print("✅ збігається")
наш цикл дав : 237.4
sum() дала   : 237.4
✅ збігається

4 · Підрахунок: скільки позицій дорожчі за 30 грн¶

Лічильник — той самий накопичувач, тільки додаємо не ціну, а одиницю. Умова всередині циклу — це if із теми 11.

In [4]:
дорогих = 0
for назва, ціна in ціни.items():
    if ціна > 30:
        дорогих += 1
        print("дорога позиція:", назва, "—", ціна)

print("усього дорожчих за 30:", дорогих)
assert дорогих == 2, "мали знайтися рівно дві: молоко і мед"
print("✅ рахунок правильний")
дорога позиція: молоко — 32.0
дорога позиція: мед — 145.0
усього дорожчих за 30: 2
✅ рахунок правильний

5 · Пошук: break і else¶

break виходить із циклу, щойно знайшли. Гілка else виконається лише тоді, коли break не спрацював — тобто цикл дійшов до кінця сам.

Запустимо той самий пошук двічі: для наявного товару й для відсутнього.

In [5]:
print("шукаємо два різні товари одним і тим самим кодом\n")

# перший пошук — товар є
шукане = "мед"
for назва in покупки:
    print("  перевіряю:", назва)
    if назва == шукане:
        print("  ЗНАЙШОВ:", назва)
        знайдено_мед = True
        break
else:
    знайдено_мед = False
    print("  немає такого в кошику")

print()

# другий пошук — товару немає
шукане = "сир"
for назва in покупки:
    print("  перевіряю:", назва)
    if назва == шукане:
        print("  ЗНАЙШОВ:", назва)
        знайдено_сир = True
        break
else:
    знайдено_сир = False
    print("  немає такого в кошику")

assert знайдено_мед is True, "мед у кошику є"
assert знайдено_сир is False, "сиру в кошику немає"
print("\n✅ обидва пошуки дали очікуваний результат")
шукаємо два різні товари одним і тим самим кодом

  перевіряю: хліб
  перевіряю: молоко
  перевіряю: яблука
  перевіряю: мед
  ЗНАЙШОВ: мед

  перевіряю: хліб
  перевіряю: молоко
  перевіряю: яблука
  перевіряю: мед
  перевіряю: сіль
  немає такого в кошику

✅ обидва пошуки дали очікуваний результат

6 · Порожній список — і чому else однаково спрацює¶

Найконтрінтуїтивніший випадок: тіло циклу не виконалось жодного разу, а гілка else виконалась. Бо її умова — не «чи були кроки», а «чи був break».

In [6]:
порожній_кошик = []
кроків = 0

for назва in порожній_кошик:
    кроків += 1
    print("цей рядок не виконається ніколи")
else:
    print("else виконався, хоча кроків було:", кроків)

assert кроків == 0, "у порожньому списку тіло не виконується"
print("✅ else залежить від break, а не від кількості кроків")
else виконався, хоча кроків було: 0
✅ else залежить від break, а не від кількості кроків

7 · Обхід рядка: збираємо слово навпаки¶

Рядок — послідовність символів, тому обходиться так само, як список. Порівняємо результат зі зрізом [::-1] із теми 05.

In [7]:
слово = "молоко"
навпаки = ""
for символ in слово:
    навпаки = символ + навпаки    # приклеюємо СПЕРЕДУ — саме це й перевертає слово

print("вихідне слово:", слово)
print("наш цикл дав :", навпаки)
print("зріз [::-1]  :", слово[::-1])
assert навпаки == слово[::-1], "перевертання розійшлося зі зрізом"
print("✅ збігається")
вихідне слово: молоко
наш цикл дав : околом
зріз [::-1]  : околом
✅ збігається

8 · Обхід словника: три різні вікна¶

for k in словник дає ключі, .values() — значення, .items() — пари, які заголовок циклу одразу розпаковує у дві змінні.

In [8]:
print("1) по ключах:")
for назва in ціни:
    print("   ", назва)

print("\n2) по значеннях:")
for ціна in ціни.values():
    print("   ", ціна)

print("\n3) парами через .items():")
for назва, ціна in ціни.items():
    print("   ", назва, "->", ціна)
1) по ключах:
    хліб
    молоко
    яблука
    мед
    сіль

2) по значеннях:
    28.5
    32.0
    19.9
    145.0
    12.0

3) парами через .items():
    хліб -> 28.5
    молоко -> 32.0
    яблука -> 19.9
    мед -> 145.0
    сіль -> 12.0

9 · Обхід множини: працює, але порядок не гарантований¶

Множина з теми 09 не впорядкована. Якщо потрібен стабільний вивід — обходь sorted().

In [9]:
знижки = {"мед", "сіль", "хліб"}

print("як віддає множина (порядок довільний):")
for назва in знижки:
    print("   ", назва)

print("\nяк віддає sorted() — завжди однаково:")
for назва in sorted(знижки):
    print("   ", назва)

assert sorted(знижки) == ["мед", "сіль", "хліб"], "sorted має давати сталий порядок"
print("\n✅ sorted() робить обхід передбачуваним")
як віддає множина (порядок довільний):
    мед
    сіль
    хліб

як віддає sorted() — завжди однаково:
    мед
    сіль
    хліб

✅ sorted() робить обхід передбачуваним

10 · range: три форми — і чому це не список¶

range зберігає лише старт, стоп і крок, а числа обчислює на льоту. Мільйон значень коштує стільки ж байтів, скільки й п'ять.

In [10]:
import sys

print("range(5)         ->", list(range(5)))
print("range(2, 6)      ->", list(range(2, 6)))
print("range(2, 12, 3)  ->", list(range(2, 12, 3)))
print("range(10, 0, -2) ->", list(range(10, 0, -2)))
print("range(5, 1)      ->", list(range(5, 1)), "  <- порожній, а не помилка")

print("\nсам обʼєкт range:", range(5), "— не список")
print("байтів у range(1_000_000):", sys.getsizeof(range(1_000_000)))
print("байтів у list(range(1_000_000)):", sys.getsizeof(list(range(1_000_000))))

# формула кількості: ceil((стоп - старт) / крок)
import math
assert len(range(2, 12, 3)) == math.ceil((12 - 2) / 3) == 4
assert len(range(5, 1)) == 0, "старт правіше за стоп при кроці +1 -> порожньо"
print("\n✅ формула кількості справдилась")
range(5)         -> [0, 1, 2, 3, 4]
range(2, 6)      -> [2, 3, 4, 5]
range(2, 12, 3)  -> [2, 5, 8, 11]
range(10, 0, -2) -> [10, 8, 6, 4, 2]
range(5, 1)      -> []   <- порожній, а не помилка

сам обʼєкт range: range(0, 5) — не список
байтів у range(1_000_000): 48
байтів у list(range(1_000_000)): 8000056

✅ формула кількості справдилась

11 · enumerate: пронумерований чек¶

Індекс потрібен лише для показу людині, тому беремо start=1. Він зсуває лічильник, а не початок обходу.

In [11]:
for номер, назва in enumerate(покупки, start=1):
    print(номер, ".", назва, "—", ціни[назва], "грн")

# і те саме без enumerate, «по-старому» — щоб побачити, наскільки довше
номери = []
for i in range(len(покупки)):
    номери.append((i + 1, покупки[i]))

через_enumerate = list(enumerate(покупки, start=1))
print("\nrange(len(...)) дав:", номери[:2], "...")
print("enumerate  дав    :", через_enumerate[:2], "...")
assert номери == через_enumerate, "два записи мають давати те саме"
print("✅ результат однаковий, але enumerate коротший і працює не лише зі списком")
1
 . хліб — 28.5 грн
2 . молоко — 32.0 грн
3 . яблука — 19.9 грн
4 . мед — 145.0 грн
5 . сіль — 12.0 грн

range(len(...)) дав: [(1, 'хліб'), (2, 'молоко')] ...
enumerate  дав    : [(1, 'хліб'), (2, 'молоко')] ...
✅ результат однаковий, але enumerate коротший і працює не лише зі списком

12 · zip: рахуємо чек по двох списках¶

Назви в одному списку, кількості в другому. zip зчіплює їх позиційно.

In [12]:
до_сплати = 0
for назва, скільки in zip(покупки, кількості):
    рядок_чека = ціни[назва] * скільки
    до_сплати += рядок_чека
    print(f"{назва:<8} × {скільки}  =  {рядок_чека:>6.2f}")

print("-" * 26)
print(f"{'до сплати':<8}      {до_сплати:>8.2f}")

# перевірка руками: 28.5*2 + 32.0*1 + 19.9*3 + 145.0*1 + 12.0*1
вручну = 28.5 * 2 + 32.0 * 1 + 19.9 * 3 + 145.0 * 1 + 12.0 * 1
assert round(до_сплати, 2) == round(вручну, 2) == 305.7
print("✅ чек збігається з ручним підрахунком")
хліб     × 2  =   57.00
молоко   × 1  =   32.00
яблука   × 3  =   59.70
мед      × 1  =  145.00
сіль     × 1  =   12.00
--------------------------
до сплати        305.70
✅ чек збігається з ручним підрахунком

13 · zip мовчки обрізає по найкоротшому¶

Найтихіша пастка теми: якщо списки різної довжини, зайві елементи просто не потраплять у цикл. Ані помилки, ані попередження.

In [13]:
неповні_кількості = [2, 1, 3, 1]        # на один менше, ніж покупок

пари = []
for назва, скільки in zip(покупки, неповні_кількості):
    пари.append(назва)

print("покупок           :", len(покупки))
print("кількостей        :", len(неповні_кількості))
print("пар утворилось    :", len(пари))
print("хто НЕ потрапив   :", set(покупки) - set(пари))

assert len(пари) == 4, "zip зупиняється на найкоротшому"
assert "сіль" not in пари, "сіль лишилась без пари й не потрапила в цикл"
print("✅ пастка відтворена: обхід тихо обрізався")
покупок           :
 5
кількостей        : 4
пар утворилось    : 4
хто НЕ потрапив   : {'сіль'}
✅ пастка відтворена: обхід тихо обрізався

Якщо різна довжина — це помилка в даних, а не задум, у Python 3.10+ можна попросити явну перевірку через strict=True. Тоді замість мовчання буде ValueError.

In [14]:
try:
    for назва, скільки in zip(покупки, неповні_кількості, strict=True):
        pass
except ValueError as помилка:
    print("ValueError:", помилка)
    спрацювало = True

assert спрацювало, "strict=True мав кинути ValueError"
print("✅ strict=True перетворює тихий баг на гучну помилку")
ValueError: zip() argument 2 is shorter than argument 1
✅ strict=True перетворює тихий баг на гучну помилку

14 · while: кроків невідомо наперед¶

Купуємо найдешевшу позицію, поки в гаманці вистачає грошей. Скільки разів це вийде — заздалегідь ми не знаємо, тому for тут не підходить.

In [15]:
import math

гаманець = 100.0
найдешевше = min(ціни.values())
куплено = 0

while гаманець >= найдешевше:
    гаманець -= найдешевше
    куплено += 1
    print(f"куплено {куплено}-ту пачку, у гаманці лишилось {гаманець:.2f}")

print("\nнайдешевша позиція коштує:", найдешевше)
print("куплено всього           :", куплено)
print("залишок                  :", round(гаманець, 2))

assert куплено == math.floor(100.0 / найдешевше) == 8
assert гаманець < найдешевше, "цикл вийшов саме тоді, коли умова стала хибною"
print("✅ кількість кроків збіглася з формулою")
куплено 1-ту пачку, у гаманці лишилось 88.00
куплено 2-ту пачку, у гаманці лишилось 76.00
куплено 3-ту пачку, у гаманці лишилось 64.00
куплено 4-ту пачку, у гаманці лишилось 52.00
куплено 5-ту пачку, у гаманці лишилось 40.00
куплено 6-ту пачку, у гаманці лишилось 28.00
куплено 7-ту пачку, у гаманці лишилось 16.00
куплено 8-ту пачку, у гаманці лишилось 4.00

найдешевша позиція коштує: 12.0
куплено всього           : 8
залишок                  : 4.0
✅ кількість кроків збіглася з формулою

15 · Чого бракує нескінченному циклу¶

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

In [16]:
гаманець = 100.0
крок = 0.0                 # ← ось вона, помилка: тіло нічого не змінює
повторень = 0
ЗАПОБІЖНИК = 50

while гаманець > 0:
    гаманець -= крок       # виконується справно — і не змінює нічого
    повторень += 1
    if повторень >= ЗАПОБІЖНИК:
        print("запобіжник спинив цикл після", повторень, "повторень")
        break

print("гаманець після", повторень, "повторень:", гаманець)
print("умова гаманець > 0 усе ще:", гаманець > 0)

assert гаманець == 100.0, "значення не змінилось жодного разу"
assert повторень == ЗАПОБІЖНИК, "без запобіжника цикл крутився б вічно"
print("✅ саме так виглядає нескінченний цикл зсередини")
запобіжник спинив цикл після 50 повторень
гаманець після 50 повторень: 100.0
умова гаманець > 0 усе ще: True
✅ саме так виглядає нескінченний цикл зсередини

16 · break проти continue на однакових даних¶

Один і той самий цикл, змінюється лише одне слово. Три різні відповіді — рівно ті самі, що в інтерактиві 4 лекції.

In [17]:
список_цін = [28.5, 32.0, 19.9, 145.0, 12.0]

# А · без керування — додаємо все
разом_pass = 0
for ціна in список_цін:
    if ціна > 100:
        pass                       # заглушка: нічого не робимо, і потік іде далі
    разом_pass += ціна

# Б · continue — обриваємо ПОТОЧНИЙ крок, цикл живе далі
разом_continue = 0
for ціна in список_цін:
    if ціна > 100:
        continue
    разом_continue += ціна

# В · break — виходимо з циклу назовсім
разом_break = 0
for ціна in список_цін:
    if ціна > 100:
        break
    разом_break += ціна

print("pass     ->", round(разом_pass, 2), " (додались усі пʼять)")
print("continue ->", round(разом_continue, 2), " (мед пропущено, сіль додано)")
print("break    ->", round(разом_break, 2), " (на меді вийшли, солі не бачили)")

assert round(разом_pass, 2) == 237.4
assert round(разом_continue, 2) == 92.4
assert round(разом_break, 2) == 80.4
print("\n✅ усі три числа збіглися з лекцією")
pass     -> 237.4  (додались усі пʼять)
continue -> 92.4  (мед пропущено, сіль додано)
break    -> 80.4  (на меді вийшли, солі не бачили)

✅ усі три числа збіглися з лекцією

17 · Вкладені цикли: таблиця множення¶

Внутрішній цикл проходить повністю на кожен крок зовнішнього. Стеж за відступами: print() без аргументів належить зовнішньому циклу — він переводить рядок.

In [18]:
рядків, стовпців = 5, 5
таблиця = []

for рядок in range(1, рядків + 1):
    рядок_чисел = []
    for стовпець in range(1, стовпців + 1):
        добуток = рядок * стовпець
        рядок_чисел.append(добуток)
        print(f"{добуток:>4}", end="")
    print()                      # цей print — зовнішнього циклу, один раз на рядок
    таблиця.append(рядок_чисел)

print("\nтіло внутрішнього циклу виконалось разів:", рядків * стовпців)
assert len(таблиця) == 5 and len(таблиця[0]) == 5
assert таблиця[2][3] == 3 * 4 == 12, "третій рядок, четвертий стовпець"
assert таблиця[4][4] == 25
print("✅ таблиця побудована правильно")
   1   2   3   4   5
   2   4   6   8  10
   3   6   9  12  15
   4   8  12  16  20
   5  10  15  20  25

тіло внутрішнього циклу виконалось разів: 25
✅ таблиця побудована правильно

18 · Пастка: зміна списку під час обходу¶

А тепер найдорожча помилка теми. У кошик випадково потрапило два сири — прибираємо їх прямо під час обходу. Помилки не буде. Відповідь буде неправильна.

In [19]:
кошик = ["хліб", "сир", "сир", "мед"]
print("до циклу :", кошик)

for назва in кошик:
    if назва == "сир":
        кошик.remove(назва)

print("після    :", кошик)
print("сирів лишилось:", кошик.count("сир"))

assert кошик == ["хліб", "сир", "мед"], "саме такий неправильний результат і має вийти"
assert кошик.count("сир") == 1, "один сир проскочив повз обхід"
print("⚠️  один сир вижив — і жодної помилки при цьому не було")
до циклу : ['хліб', 'сир', 'сир', 'мед']
після    : ['хліб', 'сир', 'мед']
сирів лишилось: 1
⚠️  один сир вижив — і жодної помилки при цьому не було

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

In [20]:
кошик = ["хліб", "сир", "сир", "мед"]
i = 0
print("імітуємо те, що робить for усередині:\n")
while i < len(кошик):
    назва = кошик[i]
    print(f"  i = {i}, беремо {назва!r}, список зараз {кошик}")
    if назва == "сир":
        кошик.remove(назва)
        print(f"       видалили -> список став {кошик}, а i все одно стане {i + 1}")
    i += 1

print("\nрезультат:", кошик)
assert кошик == ["хліб", "сир", "мед"], "та сама неправильна відповідь, що й у справжньому for"
print("✅ ручна імітація дала точно те саме — отже, причина саме в лічильнику")
імітуємо те, що робить for усередині:

  i = 0, беремо 'хліб', список зараз ['хліб', 'сир', 'сир', 'мед']
  i = 1, беремо 'сир', список зараз ['хліб', 'сир', 'сир', 'мед']
       видалили -> список став ['хліб', 'сир', 'мед'], а i все одно стане 2
  i = 2, беремо 'мед', список зараз ['хліб', 'сир', 'мед']

результат: ['хліб', 'сир', 'мед']
✅ ручна імітація дала точно те саме — отже, причина саме в лічильнику

Як робити правильно¶

Три робочі способи. Найчастіший — обхід копії: копію ніхто не зсуває, бо змінюємо ми не її.

In [21]:
# спосіб 1 · обходимо копію, змінюємо оригінал
кошик = ["хліб", "сир", "сир", "мед"]
for назва in кошик[:]:
    if назва == "сир":
        кошик.remove(назва)
спосіб_1 = кошик

# спосіб 2 · збираємо новий список замість того, щоб псувати старий
кошик = ["хліб", "сир", "сир", "мед"]
залишок = []
for назва in кошик:
    if назва != "сир":
        залишок.append(назва)
спосіб_2 = залишок

# спосіб 3 · ідемо з кінця, коли потрібні саме індекси
кошик = ["хліб", "сир", "сир", "мед"]
for i in range(len(кошик) - 1, -1, -1):
    if кошик[i] == "сир":
        кошик.pop(i)
спосіб_3 = кошик

print("копія     ->", спосіб_1)
print("новий     ->", спосіб_2)
print("з кінця   ->", спосіб_3)

assert спосіб_1 == спосіб_2 == спосіб_3 == ["хліб", "мед"]
print("✅ усі три способи прибрали обидва сири")
копія     -> ['хліб', 'мед']
новий     -> ['хліб', 'мед']
з кінця   -> ['хліб', 'мед']
✅ усі три способи прибрали обидва сири

Зі словником пастка гучніша: він не дає мовчки зіпсувати дані, а одразу кидає RuntimeError. Наступна клітинка навмисно падає — traceback тут і є навчальним матеріалом.

In [22]:
запаси = {"хліб": 0, "молоко": 3, "яблука": 0}

for назва, скільки in запаси.items():
    if скільки == 0:
        del запаси[назва]        # ← RuntimeError: dictionary changed size during iteration
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In[22], line 3
      1 запаси = {"хліб": 0, "молоко": 3, "яблука": 0}
----> 3 for назва, скільки in запаси.items():
      4     if скільки == 0:
      5         del запаси[назва]        # ← RuntimeError: dictionary changed size during iteration

RuntimeError: dictionary changed size during iteration

Ліки ті самі: обходь копію ключів. list(запаси) фіксує ключі до початку циклу.

In [23]:
запаси = {"хліб": 0, "молоко": 3, "яблука": 0}

for назва in list(запаси):       # list() робить копію ключів ДО обходу
    if запаси[назва] == 0:
        del запаси[назва]

print("лишилось у запасах:", запаси)
assert запаси == {"молоко": 3}, "мали зникнути обидві нульові позиції"
print("✅ тепер словник змінюється безпечно")
лишилось у запасах: {'молоко': 3}
✅ тепер словник змінюється безпечно

19 · Ітератор одноразовий¶

zip і enumerate — не списки, а ітератори: вони пам'ятають позицію й уміють іти лише вперед. Другий обхід того самого об'єкта не дасть нічого, і помилки при цьому теж не буде.

In [24]:
пари = zip(покупки, кількості)

перший_прохід = 0
for назва, скільки in пари:
    перший_прохід += 1

другий_прохід = 0
for назва, скільки in пари:      # той самий обʼєкт, уже вичерпаний
    другий_прохід += 1

print("кроків на першому проході:", перший_прохід)
print("кроків на другому проході:", другий_прохід)

assert перший_прохід == 5
assert другий_прохід == 0, "ітератор одноразовий — другий обхід порожній"
print("✅ пастка відтворена: помилки немає, кроків теж")
кроків на першому проході: 5
кроків на другому проході: 0
✅ пастка відтворена: помилки немає, кроків теж

Побачити механіку зсередини можна руками: iter() просить ітератор, next() бере наступний елемент, а коли елементи скінчились — прилітає StopIteration. Саме його for ловить і тихо завершує цикл.

In [25]:
обхідник = iter(["хліб", "мед"])
print("сам обʼєкт:", обхідник)
print("next() ->", next(обхідник))
print("next() ->", next(обхідник))

try:
    next(обхідник)
except StopIteration:
    print("next() -> StopIteration: елементи скінчились")
    кінець = True

assert кінець, "третій виклик next() мав дати StopIteration"
print("✅ ось що робить for на кожному кроці — і чому він завершується сам")
сам обʼєкт: <list_iterator object at 0x7f8143db72e0>
next() -> хліб
next() -> мед
next() -> StopIteration: елементи скінчились
✅ ось що робить for на кожному кроці — і чому він завершується сам

Ліки прості: якщо результат потрібен більше одного разу, зафіксуй його списком.

In [26]:
пари = list(zip(покупки, кількості))   # список, а не ітератор

перший = 0
for _ in пари:
    перший += 1

другий = 0
for _ in пари:
    другий += 1

print("перший прохід:", перший, "· другий прохід:", другий)
assert перший == другий == 5, "список можна обходити скільки завгодно разів"
print("✅ список обходиться повторно, ітератор — ні")
перший прохід: 5 · другий прохід: 5
✅ список обходиться повторно, ітератор — ні

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

🟢 Рівень 1¶

Візьми свій список покупок (щонайменше шість позицій) і словник цін до нього. Одним циклом надрукуй пронумерований чек через enumerate(…, start=1), другим — порахуй суму. Зроблено, якщо сума з твого циклу проходить assert round(разом, 2) == round(sum(ціни.values()), 2).

🟡 Рівень 2¶

Напиши цикл, який шукає в кошику найдорожчу позицію без вбудованої max(): заведи змінні найдорожча_назва й найдорожча_ціна до циклу й оновлюй їх усередині. Зроблено, якщо результат збігається з max(ціни, key=ціни.get), а на порожньому словнику твій код не падає, а друкує «кошик порожній».

🔴 Рівень 3¶

Напиши цикл, який прибирає з кошика всі позиції, дорожчі за задану межу, не створюючи нового списку — тобто змінюючи оригінал на місці. Перевір свій розвʼязок на списку, де дорогі позиції стоять підряд (саме там ламається наївний обхід). Зроблено, якщо для [10, 200, 300, 20] і межі 100 результат [10, 20], і ти письмово пояснив, чому обхід копії або рух із кінця тут обовʼязковий.