Перейти до змісту

Урок 5. Списки, кортежі та множини

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

Щоб програма пам'ятала багато значень одразу, потрібні колекції. У цьому уроці — три основні:

  • список (list) — впорядкований набір, який можна змінювати;
  • кортеж (tuple) — впорядкований запис, який після створення не змінюється;
  • множина (set) — набір унікальних значень без дублікатів.

Що потрібно з попередніх уроків: змінні й типи (урок 3), індекси рядка ("Charkiv"[0]), цикл while, умови і in (урок 4).

Після уроку ти зможеш:

  • створювати списки, читати елементи за індексом і зрізом, додавати й видаляти елементи;
  • пояснювати, чому два імені можуть змінювати один і той самий список, і як зробити копію;
  • зберігати фіксований запис у кортежі, розпаковувати його і давати полям імена через NamedTuple;
  • проходити список циклом while і рахувати суму, середнє та максимум;
  • прибирати дублікати і порівнювати набори значень через множини;
  • обирати контейнер під задачу: list, tuple чи set.

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

Ноутбук заняття: Відкрити вправи в Colab Переглянути розв’язки — нотатка про три типи: усі методи з прикладами, типові помилки, шпаргалка, 244 реальних чеки і задачі з перевірками.

Розповідь заняття: konspekt_lesson_05_cafe_story.ipynb Open In Colab — одна зміна в кафе, у якій по черзі з'являються список, кортеж і множина; за нею іде заняття. Схеми до неї — restoran_alg.md.

Пригадай

Дай відповідь подумки, нічого не запускаючи:

  1. Що поверне 5 > 3 and 2 > 10?
  2. Скільки разів виконається блок циклу while count < 3:, якщо count = 0, а в блоці є count += 1?
  3. Що дасть "Charkiv"[0] і що — "Charkiv"[-1]?
Відповіді
  1. False: and вимагає, щоб обидві частини були істинні, а 2 > 10 хибне.
  2. Три рази: для count = 0, 1, 2. На перевірці 3 < 3 цикл завершується.
  3. 'C' — перший символ (індекс 0), 'v' — останній (індекс -1). Так само, як у рядках, працюватимуть індекси списків.

Список: набір, що змінюється

Створення і читання

Список записують у квадратних дужках, елементи — через кому. Замовлення за столиком:

items = ["кава", "круасан", "чай", "кава"]

print(items)
print(len(items))
['кава', 'круасан', 'чай', 'кава']
4

Список зберігає порядок і дозволяє повтори: дві кави — два окремі елементи. len() повертає кількість елементів.

До елемента звертаються за індексом — номером позиції, який починається з 0. Від'ємний індекс рахує з кінця:

print(items[0])
print(items[-1])
кава
кава
Елемент Індекс Від'ємний індекс
"кава" 0 -4
"круасан" 1 -3
"чай" 2 -2
"кава" 3 -1

Індекс за межами списку — помилка:

items[4]
IndexError: list index out of range

Останній індекс завжди len(items) - 1, тобто тут 3.

Зрізи

Зріз items[start:stop] повертає новий список з елементів від start до stop, не включаючи stop:

print(items[1:3])
print(items[:2])
print(items[2:])
print(items[::-1])
['круасан', 'чай']
['кава', 'круасан']
['чай', 'кава']
['кава', 'чай', 'круасан', 'кава']

Пропущений start означає «з початку», пропущений stop — «до кінця», а [::-1] проходить список у зворотному порядку. На відміну від індексу, зріз за межами не дає помилки: items[1:100] просто поверне все від індексу 1 до кінця.

Перевірка і зміна

in перевіряє, чи є значення в списку, а .count() рахує, скільки разів воно трапляється:

print("чай" in items)
print("піца" in items)
print(items.count("кава"))
True
False
2

Список змінний: той самий об'єкт можна змінювати на місці, не створюючи нового.

items = ["кава", "круасан", "чай", "кава"]

items[2] = "какао"        # замінити елемент
items.append("чай")       # додати в кінець
items.insert(0, "вода")   # вставити на позицію 0
print(items)

items.remove("кава")      # видалити перше входження значення
print(items)

last = items.pop()        # забрати й повернути останній елемент
print(last, items)
['вода', 'кава', 'круасан', 'какао', 'кава', 'чай']
['вода', 'круасан', 'какао', 'кава', 'чай']
чай ['вода', 'круасан', 'какао', 'кава']

Зверни увагу: .remove("кава") прибрав лише першу каву, друга лишилася. Якщо значення немає, .remove() зупиняє програму:

items.remove("піца")
ValueError: list.remove(x): x not in list

Тому перед видаленням значення, якого може не бути, перевіряй if "піца" in items:.

Сортування: sorted() і .sort()

Для списку цін є два способи впорядкувати значення, і вони поводяться по-різному:

prices = [55, 70, 40, 55]

print(sorted(prices))
print(prices)

prices.sort()
print(prices)
[40, 55, 55, 70]
[55, 70, 40, 55]
[40, 55, 55, 70]
  • sorted(prices) повертає новий відсортований список, а початковий не змінює.
  • prices.sort() змінює сам список і повертає None.

Типова помилка

prices = prices.sort() записує в prices значення None, і список втрачається. Або prices.sort() окремим рядком, або prices = sorted(prices).

Для числових списків корисні також sum(), min() і max(): для [40, 55, 55, 70] вони дадуть 220, 40 і 70.

Два імені — один список

В уроці 3 ми бачили, що b = a не копіює значення, а прив'язує ще одне ім'я до того самого об'єкта. Для чисел і рядків це непомітно, бо вони незмінні. Зі списком це стає важливим. Передбач результат:

items = ["кава"]
today = items
today.append("чай")
print(items)
Що надрукує print(items)?

Подумай, скільки списків існує після рядка today = items.

Відповідь і пояснення
['кава', 'чай']

Список лише один. today = items прив'язало до нього друге ім'я, а .append() змінило сам список. Тому зміну видно через обидва імена.

Щоб отримати незалежний список, його копіюють: backup = items.copy().

flowchart TD
    classDef step     fill:#eceff1,stroke:#546e7a,stroke-width:1px,color:#1a1a1a;
    classDef decision fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#1a1a1a;
    classDef success  fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#1a1a1a;
    classDef error    fill:#ffebee,stroke:#c62828,stroke-width:3px,color:#1a1a1a;
    classDef warning  fill:#fff8e1,stroke:#e65100,stroke-width:2px,color:#1a1a1a;

    subgraph S1["Стан 1: today = items, потім today.append('чай')"]
        direction TB
        I1(["ім'я items"]) -- посилається --> L1["один список<br>['кава', 'чай']"]
        T1(["ім'я today"]) -- посилається --> L1
    end

    subgraph S2["Стан 2: backup = items.copy(), потім backup.append('вода')"]
        direction TB
        I2(["ім'я items"]) -- посилається --> L2["['кава', 'чай']"]
        B2(["ім'я backup"]) -- посилається --> L3["окрема копія<br>['кава', 'чай', 'вода']"]
    end

    S1 --> S2

    class I1,T1,I2,B2 decision
    class L1,L2 warning
    class L3 success

У першому стані обидві стрілки ведуть до одного списку, тому зміна через today видна і через items. У другому .copy() створила новий список, і .append() змінює лише копію.

Прохід списком через while

Щоб обробити кожен елемент, потрібен цикл. Поки що в нас є while: заводимо індекс i, починаємо з 0 і збільшуємо, доки він менший за довжину списку. Порахуймо кави в замовленні:

items = ["кава", "чай", "кава"]

coffee = 0
i = 0
while i < len(items):
    if items[i] == "кава":
        coffee += 1
    i += 1

print("Кав у замовленні:", coffee)
Кав у замовленні: 2
i items[i] coffee після кроку
0 "кава" 1
1 "чай" 1
2 "кава" 2
3 — 3 < 3 хибне, цикл завершено

Умова i < len(items) гарантує, що індекс ніколи не вийде за межі: останній опрацьований індекс — 2, тобто len(items) - 1.

Що станеться, якщо написати while i <= len(items):?

Дійди в таблиці до i = 3.

Відповідь

При i = 3 умова 3 <= 3 ще істинна, і рядок items[3] зупинить програму з IndexError: list index out of range. Це та сама помилка на одиницю, що й в уроці 4, але тут вона не додає зайвий повтор, а ламає програму.

Для задачі «скільки разів трапляється значення» є готовий items.count("кава"). Але той самий цикл знадобиться для сум, середніх і максимумів, для яких готового методу немає. В уроці 6 цей цикл стане коротшим завдяки for, а поки що індекс керуємо вручну.

Кортеж: запис, що не змінюється

Закритий чек

Коли чек закрито, його дані не повинні змінюватися: номер столика, сума, чайові. Для такого фіксованого запису підходить кортеж — значення в круглих дужках через кому:

receipt = (7, 540.0, 50.0)

print(receipt[0])
print(receipt[-1])
print(len(receipt))
7
50.0
3

Індекси й зрізи працюють так само, як у списку. Але змінити кортеж не можна:

receipt[1] = 600.0
TypeError: 'tuple' object does not support item assignment

У кортежу немає .append(), .remove() та інших методів, що змінюють вміст. Щоб «змінити» кортеж, створюють новий.

Пакування і розпакування

Значення через кому самі складаються в кортеж, навіть без дужок. Це пакування:

point = 10, 20
print(type(point), point)
<class 'tuple'> (10, 20)

Зворотна дія — розпакування: кортеж розкладають по змінних за одну дію.

receipt = (7, 540.0, 50.0)
table, total, tip = receipt
print(table, total, tip)
7 540.0 50.0

Так само працює обмін значень місцями без тимчасової змінної:

x, y = 1, 2
x, y = y, x
print(x, y)
2 1

Кількість змінних зліва має дорівнювати кількості значень:

a, b = (7, 540.0, 50.0)          # ValueError: too many values to unpack (expected 2)
a, b, c, d = (7, 540.0, 50.0)    # ValueError: not enough values to unpack (expected 4, got 3)

Кортеж з одного елемента

Кортеж створюють коми, а не дужки. (42) — це просто число 42 у дужках, тип int. Кортеж з одного елемента потребує коми: (42,).

Незмінний — але що саме?

Кортеж не дає замінити свої елементи. Але якщо елемент сам є змінним об'єктом, наприклад списком, цей список можна змінити:

t = (1, [2, 3])
t[1].append(4)
print(t)       # (1, [2, 3, 4])

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

Коли кортеж, а коли список

list tuple
Розмір змінюється? так ні, форма фіксована
Що означає позиція? усі елементи рівноправні кожна позиція має свій сенс
Приклад у кафе позиції замовлення один закритий чек

NamedTuple: поля з іменами

Проблема індексів

У записі receipt = (7, 540.0, 50.0) доводиться пам'ятати, що receipt[1] — це сума, а receipt[2] — чайові. Через тиждень, або коли полів стане п'ять, легко переплутати індекс. Python нічого не помітить: код мовчки порахує не те.

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

from typing import NamedTuple


class Order(NamedTuple):
    waiter: str
    total_bill: float
    tip: float
    day: str
    time: str
    size: int


order = Order("Тарас", 540.0, 50.0, "пт", "вечеря", 2)

print(order)
print(order.waiter, order.total_bill, order.tip)
Order(waiter='Тарас', total_bill=540.0, tip=50.0, day='пт', time='вечеря', size=2)
Тарас 540.0 50.0

Як це читати:

  • from typing import NamedTuple підключає інструмент з модуля typing (як import random в уроці 4);
  • блок class Order(NamedTuple): — шаблон запису: назва Order і список полів. Після двокрапки в кожному рядку — назва поля і тип, який ми туди очікуємо;
  • Order("Тарас", 540.0, 50.0, "пт", "вечеря", 2) створює один чек: значення розкладаються по полях у тому порядку, в якому поля оголошено.

Слово class належить до теми класів, яку розберемо в модулі 2. Поки що достатньо сприймати цей блок як опис полів запису. Поля ті самі, що й у розповіді заняття, квесті та ноутбуці: хто обслуговував, сума чека, чайові, день, прийом їжі, кількість гостей.

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

print(isinstance(order, tuple))
print(order[1])

waiter, total_bill, tip, day, time, size = order
print(waiter, day, size)

order.tip = 70.0
True
540.0
Тарас пт 2
AttributeError: can't set attribute

Типи в шаблоні — це підказка

Записи total_bill: float описують, що ми очікуємо в полі, але під час виконання Python їх не перевіряє. Order("Тарас", "п'ятсот", 50.0, "пт", "вечеря", 2) створиться без помилки, а проблема з'явиться пізніше, коли з полем спробують рахувати.

Список чеків дня

Один чек — Order. Усі чеки дня — список таких записів:

orders = [
    Order("Тарас", 540.0, 50.0, "пт", "вечеря", 2),
    Order("Марія", 320.0, 30.0, "пт", "обід", 1),
    Order("Тарас", 980.0, 120.0, "сб", "вечеря", 4),
    Order("Олексій", 760.0, 70.0, "сб", "вечеря", 3),
    Order("Марія", 450.0, 0.0, "нд", "обід", 5),
]

print(len(orders))
print(orders[2].total_bill, orders[2].waiter)
5
980.0 Тарас

orders[2] — третій чек у списку (індекс 2), а .total_bill і .waiter — поля цього чека: сума і хто його обслужив.

flowchart TD
    classDef step     fill:#eceff1,stroke:#546e7a,stroke-width:1px,color:#1a1a1a;
    classDef decision fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#1a1a1a;
    classDef success  fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#1a1a1a;
    classDef error    fill:#ffebee,stroke:#c62828,stroke-width:3px,color:#1a1a1a;
    classDef warning  fill:#fff8e1,stroke:#e65100,stroke-width:2px,color:#1a1a1a;

    L["orders — list<br>5 чеків, можна додавати"] --> O2["orders[2] — Order<br>незмінний запис"]
    L --> ON["інші чеки<br>індекси 0, 1, 3, 4"]
    O2 --> F1["orders[2].total_bill<br>980.0"]
    O2 --> F2["orders[2].waiter<br>'Тарас'"]

    class L decision
    class O2,ON step
    class F1,F2 success

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

Найбільший чек: поточний максимум

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

max_bill = orders[0].total_bill
i = 1
while i < len(orders):
    if orders[i].total_bill > max_bill:
        max_bill = orders[i].total_bill
    i += 1

print("Найбільший чек:", max_bill)
Найбільший чек: 980.0
i Сума чека max_bill після кроку
старт 540.0 (чек 0) 540.0
1 320.0 — не більше 540.0
2 980.0 — більше 980.0
3 760.0 — не більше 980.0
4 450.0 — не більше 980.0

Та сама таблиця покроково — помаранчевим позначено порівняння, зеленим — оновлення максимуму:

flowchart TD
    classDef step     fill:#eceff1,stroke:#546e7a,stroke-width:1px,color:#1a1a1a;
    classDef decision fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#1a1a1a;
    classDef success  fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#1a1a1a;
    classDef error    fill:#ffebee,stroke:#c62828,stroke-width:3px,color:#1a1a1a;
    classDef warning  fill:#fff8e1,stroke:#e65100,stroke-width:2px,color:#1a1a1a;

    S["старт<br>max_bill = 540.0 (чек 0)"]
    subgraph I1["i = 1"]
        direction LR
        C1{"320.0 > 540.0?"} -- ні --> K1["max_bill = 540.0"]
    end
    subgraph I2["i = 2"]
        direction LR
        C2{"980.0 > 540.0?"} -- так --> U2["max_bill = 980.0"]
    end
    subgraph I3["i = 3"]
        direction LR
        C3{"760.0 > 980.0?"} -- ні --> K3["max_bill = 980.0"]
    end
    subgraph I4["i = 4"]
        direction LR
        C4{"450.0 > 980.0?"} -- ні --> K4["max_bill = 980.0"]
    end
    S --> I1 --> I2 --> I3 --> I4 --> E["i = 5: 5 < 5 — ні<br>Найбільший чек: 980.0"]

    class S step
    class C1,C2,C3,C4 warning
    class K1,K3,K4 step
    class U2,E success

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

Цикл починається з i = 1, бо чек 0 уже взято як стартовий максимум. Чому стартують з першого елемента, а не з 0? Якби всі чеки були від'ємні (наприклад, повернення коштів), нуль став би «максимумом», якого немає в даних.

Множина: лише унікальні значення

Дублікати зникають самі

Множину записують у фігурних дужках. Однакові значення в ній зберігаються лише один раз:

tables = {3, 1, 2, 3}
print(tables)
print(len(tables))
{1, 2, 3}
3

Порожню множину створюють лише через set(), бо {} — це порожній словник (тема уроку 6):

days = {}
days.add("пт")
AttributeError: 'dict' object has no attribute 'add'

Правильно: days = set(), а потім days.add("пт"). Додавання значення, яке вже є, нічого не змінює й помилки не дає.

Що вміє множина, а чого ні

  • "пт" in days — перевірка належності. Саме для цього множини найкорисніші: in для множини працює швидко навіть на мільйонах елементів, а список доводиться переглядати по одному.
  • days.remove("пн") дає KeyError: 'пн', якщо значення немає. days.discard("пн") тихо нічого не робить.
  • Індексів немає: days[0] дає TypeError: 'set' object is not subscriptable. У множини немає «першого» елемента.
  • Порядок не гарантований: Python показує елементи в зручному йому порядку, не в порядку додавання. Якщо порядок потрібен для виводу, використовуй sorted(days) — він поверне відсортований список.
  • Елементи мають бути незмінними: числа, рядки, кортежі. Список у множину покласти не можна: {[1, 2], 3} дає TypeError: unhashable type: 'list'.

Унікальні дні з чеків

Скільки різних днів працювало кафе? Проходимо чеки й додаємо день кожного в множину:

days = set()
i = 0
while i < len(orders):
    days.add(orders[i].day)
    i += 1

print(len(days))
print(sorted(days))
3
['нд', 'пт', 'сб']

П'ять чеків, але днів лише три: повторні "пт" і "сб" множина не додала. sorted() впорядкував назви за абеткою, а не за днями тижня: для множини порядок днів ніхто не зберігав.

Для простого списку значень те саме робить один рядок: len(set(["кава", "чай", "кава"])) дає 2.

Хто з офіціантів працював

Той самий прохід, інше поле. Тарас і Марія мають по два чеки, але в множині кожне ім'я — один раз:

waiters = set()
i = 0
while i < len(orders):
    waiters.add(orders[i].waiter)
    i += 1

print(len(waiters))
print(sorted(waiters))
3
['Марія', 'Олексій', 'Тарас']

list(set(...)) губить порядок

list(set(items)) прибирає дублікати, але порядок елементів у результаті може відрізнятися від початкового. Якщо порядок важливий, прибирай дублікати циклом: додавай елемент у новий список, лише якщо його там ще немає.

Операції з множинами

Власник кафе хоче порівняти гостей двох днів:

monday = {"Олена", "Богдан", "Марта"}
tuesday = {"Богдан", "Марта", "Петро"}
Запис Результат Що означає
monday | tuesday Богдан, Марта, Олена, Петро об'єднання: хто приходив хоча б раз
monday & tuesday Богдан, Марта перетин: хто приходив обидва дні
monday - tuesday Олена різниця: був у понеділок, але не у вівторок
monday ^ tuesday Олена, Петро симетрична різниця: був лише в один із днів

Кожен запис повертає нову множину. Python надрукує її у фігурних дужках і в довільному порядку: у таблиці імена впорядковано для зручності.

Оператори |, &, -, ^ працюють лише між двома множинами: monday | ["Зоя"] дає TypeError: unsupported operand type(s) for |: 'set' and 'list'. Методи з назвами (.union(), .intersection(), .difference()) приймають і список: monday.union(["Зоя"]).

Як обрати контейнер

list tuple set
Змінний так ні так
Порядок так так ні
Дублікати так так ні
Індекс так так ні

У кафе: list — позиції замовлення й чеки дня, tuple — один закритий чек, set — унікальні дні та гості.

flowchart TD
    classDef step     fill:#eceff1,stroke:#546e7a,stroke-width:1px,color:#1a1a1a;
    classDef decision fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#1a1a1a;
    classDef success  fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#1a1a1a;
    classDef error    fill:#ffebee,stroke:#c62828,stroke-width:3px,color:#1a1a1a;
    classDef warning  fill:#fff8e1,stroke:#e65100,stroke-width:2px,color:#1a1a1a;

    Q1{"Важлива лише<br>унікальність і перевірка in?"} -- так --> SET["set"]
    Q1 -- ні --> Q2{"Це один запис<br>фіксованої форми?"}
    Q2 -- так --> Q3{"Поля мають<br>різний сенс?"}
    Q3 -- так --> NT["NamedTuple"]
    Q3 -- ні --> TUP["tuple"]
    Q2 -- ні --> LIST["list"]

    class Q1,Q2,Q3 decision
    class SET,NT,TUP,LIST success

Вибір — це не про синтаксис, а про те, що ти повідомляєш читачеві коду: «цей набір росте» (list), «цей запис не зміниться» (tuple), «тут важлива лише наявність» (set).

Практика

Розібраний приклад: звіт за день

Програма бере чеки дня і друкує звіт для власника: кількість чеків, виторг, частку чайових, найбільший чек і дні роботи.

from typing import NamedTuple


class Order(NamedTuple):
    waiter: str
    total_bill: float
    tip: float
    day: str
    time: str
    size: int


orders = [
    Order("Тарас", 540.0, 50.0, "пт", "вечеря", 2),
    Order("Марія", 320.0, 30.0, "пт", "обід", 1),
    Order("Тарас", 980.0, 120.0, "сб", "вечеря", 4),
    Order("Олексій", 760.0, 70.0, "сб", "вечеря", 3),
    Order("Марія", 450.0, 0.0, "нд", "обід", 5),
]

revenue = 0
tips = 0
best = orders[0]
days = set()
waiters = set()

i = 0
while i < len(orders):
    order = orders[i]
    revenue += order.total_bill
    tips += order.tip
    if order.total_bill > best.total_bill:
        best = order
    days.add(order.day)
    waiters.add(order.waiter)
    i += 1

print("Чеків:", len(orders))
print("Виторг:", revenue, "грн")
print("Чайові:", round(tips / revenue * 100, 1), "% від виторгу")
print("Найбільший чек:", best.total_bill, "грн —", best.waiter)
print("Днів роботи:", len(days), sorted(days))
print("Офіціантів:", len(waiters), sorted(waiters))
Чеків: 5
Виторг: 3050.0 грн
Чайові: 8.9 % від виторгу
Найбільший чек: 980.0 грн — Тарас
Днів роботи: 3 ['нд', 'пт', 'сб']
Офіціантів: 3 ['Марія', 'Олексій', 'Тарас']

Що відбувається в ключових рядках:

  • рядки 4–10 — шаблон запису Order, перше поле — хто обслуговував;
  • рядки 13–19 — список чеків дня: кожен елемент — один Order;
  • рядки 21–25 — початковий стан: дві суми з нуля, стартовий чемпіон — перший чек цілком (не лише його сума, щоб потім назвати офіціанта), дві порожні множини;
  • рядок 28 — while проходить індекси від 0 до len(orders) - 1;
  • рядок 29 — order = orders[i] дає коротке ім'я поточному чеку, щоб не писати orders[i] щоразу;
  • рядки 30–35 — одна ітерація оновлює все одразу: дві суми, чемпіона і дві множини;
  • рядок 36 — зміна стану: без i += 1 цикл став би нескінченним;
  • рядок 40 — чайові разом 270 грн, 270 / 3050 * 100 ≈ 8.85, а round(…, 1) округлює до одного знака після коми: 8.9.

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

Зміни приклад: великі столи

Та сама задача, що й у ноутбуці заняття. Знайди чеки, де за столом size >= 4, і виведи:

  • кількість таких чеків;
  • середній чек;
  • середній чек на одну людину — середнє значення total_bill / size для цих чеків.

Для списку orders з прикладу вище очікуваний результат:

Великих столів: 2
Середній чек: 715.0 грн
На людину: 167.5 грн

Тут підходять чеки 980.0 грн на 4 гостей і 450.0 грн на 5 гостей: (980.0 + 450.0) / 2 = 715.0, а на людину (245.0 + 90.0) / 2 = 167.5.

Критерії перевірки:

  • умова саме size >= 4: стіл на 4 гостей теж великий;
  • якщо великих столів немає (наприклад, у списку лише перші два чеки), програма пише Великих столів немає і не падає з ZeroDivisionError;
  • список orders після роботи програми не змінився.
Підказка

Потрібні три накопичувачі: лічильник чеків, сума чеків і сума значень total_bill / size. Ділити на кількість можна лише після циклу і лише тоді, коли вона більша за нуль.

Спробуй самостійно: реєстрація на воркшопи

Інший контекст, ті самі інструменти. Люди реєструвалися на два воркшопи через форму, і деякі заповнили її двічі. Є два списки email:

python_workshop = ["olena@mail.com", "taras@mail.com", "olena@mail.com", "marta@mail.com"]
git_workshop = ["marta@mail.com", "petro@mail.com", "taras@mail.com", "petro@mail.com"]

Напиши програму, яка виводить:

Python: 3 учасники
Git: 3 учасники
На обох: ['marta@mail.com', 'taras@mail.com']
Лише на Python: ['olena@mail.com']
Усього різних людей: 4
Усі учасники: ['marta@mail.com', 'olena@mail.com', 'petro@mail.com', 'taras@mail.com']

Критерії перевірки:

  • повторні реєстрації не рахуються: у кожному воркшопі — 3 людини, а не 4;
  • списки email у виводі відсортовані, тож результат однаковий на будь-якому комп'ютері;
  • якщо додати до git_workshop ще один "olena@mail.com", Олена зникне з «Лише на Python» і з'явиться в «На обох», а «Усього різних людей» лишиться 4;
  • початкові списки не змінюються.
Підказка

Перетвори кожен список на множину один раз, на початку. Далі кожен рядок виводу — одна операція з множинами і, за потреби, sorted().

Квест для груп: чи отримає Тарас чайові?

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

Служба Контейнер Що перевіряє
Кухня list замовлення змінилося: дозамовили, відмовилися, хліб на початок
Каса tuple розпакувати закритий чек і порахувати суму до сплати
Зал NamedTuple, індекси чайові останніх трьох чеків дня через від'ємні індекси
Адміністратор set постійні гості — перетин гостей двох днів

Усі групи відкривають той самий ноутбук і розв'язують свою перевірку. Каса дає підказку на типову помилку, а за правильну відповідь показує частину чайових у гривнях. Групи називають суми викладачу, він вписує їх у клітинку «Закриття зміни» — і всі дізнаються, скільки отримає Тарас. Удома ноутбук можна пройти самостійно, усі чотири перевірки.

Ноутбук квесту: Відкрити вправи в Colab Переглянути розв’язки

Підсумок

Що потрібно Як записати
створити список / кортеж / множину [1, 2], (1, 2), {1, 2}; порожня множина — set()
кількість елементів len(items)
елемент за позицією items[0], items[-1] (для list і tuple)
частина послідовності items[1:3], items[::-1]
перевірити наявність "кава" in items
змінити список .append(), .insert(), .remove(), .pop(), items[i] = …
копія списку items.copy()
відсортувати sorted(items) — новий список; items.sort() — на місці
розпакувати запис table, total, tip = receipt
запис з іменами полів class Order(NamedTuple): …, order.tip
прибрати дублікати set(items)
порівняти набори a | b, a & b, a - b, a ^ b
пройти список i = 0, while i < len(items): … i += 1

Самоперевірка

  1. Що дасть ["a", "b", "c"][1:]?
  2. Після a = [1], b = a, b.append(2) — що в a? А якщо замість b = a написати b = a.copy()?
  3. Чим (5) відрізняється від (5,)?
  4. Чому для одного закритого чека краще кортеж, а для всіх чеків дня — список?
  5. Що виведе len({"пт", "сб", "пт"}) і чому?
  6. Чому days = {} не створює порожню множину?
Відповіді
  1. ['b', 'c'] — від індексу 1 до кінця.
  2. [1, 2]: обидва імені вказують на один список. З a.copy() у a лишиться [1], бо b — окремий список.
  3. (5) — число 5, (5,) — кортеж з одного елемента. Кортеж створює кома.
  4. Закритий чек — фіксований запис, де кожна позиція має свій сенс, і його не повинні змінювати. Чеки дня — набір, що росте протягом дня.
  5. 2: множина зберігає "пт" лише один раз.
  6. {} — це порожній словник. Порожня множина — лише set().

Що далі

Документація