Практика · Кортежі й розпакування¶
Лекція: lecture.html · Тест: quiz.html · ДЗ: homework.md
Наскрізний приклад той самий, що в лекції, — координати міст на карті. Пара «широта, довгота» це саме те, що не має мінятися частинами.
Що зробимо руками:
- створимо кортежі й переконаємось, що кортеж робить кома, а не дужки;
- спіймаємо власними руками
TypeErrorіAttributeErrorвід спроби змінити кортеж; - розпакуємо дані трьома шаблонами, зокрема із зірочкою і вкладено;
- розберемо обмін
a, b = b, aі доведемоassert-ом, що він працює; - порахуємо довжину маршруту своєю формулою й звіримо з
math.dist; - побачимо, що незмінність кортежа діє лише на один рівень;
- складемо словник із кортежними ключами й зловимо
unhashable type; - приробимо до кортежа імена полів через
namedtuple.
Зошит виконується згори вниз без жодних правок. Клітинки, які мають впасти, позначені окремо — там помилка і є результатом.
1 · Кортеж координат¶
Створимо дві точки й подивимось, що з ними можна робити на читання. Усе, що ти вже знаєш про списки, тут працює так само: індекс, відʼємний індекс, довжина, перевірка на входження, зріз.
київ = (50.45, 30.52)
львів = (49.84, 24.03)
print("київ =", київ)
print("тип =", type(київ))
print("широта київ[0] =", київ[0])
print("довгота київ[-1]=", київ[-1])
print("довжина len() =", len(київ))
print("зріз київ[0:1] =", київ[0:1], " <- зріз кортежа теж кортеж")
print("30.52 in київ =", 30.52 in київ)
київ = (50.45, 30.52) тип = <class 'tuple'> широта київ[0] = 50.45 довгота київ[-1]= 30.52 довжина len() = 2 зріз київ[0:1] = (50.45,) <- зріз кортежа теж кортеж 30.52 in київ = True
Скільки методів лишилося¶
У списку методів близько сорока. У кортежа — рівно два, і обидва тільки читають.
Перевіримо це напряму: hasattr(обʼєкт, "імʼя") відповідає, чи є в обʼєкта
метод із таким іменем.
покупки = [1, 2, 3]
print("append у списку :", hasattr(покупки, "append"))
print("append у кортежа :", hasattr(київ, "append"))
print("sort у кортежа :", hasattr(київ, "sort"))
print("count у кортежа :", hasattr(київ, "count"))
print("index у кортежа :", hasattr(київ, "index"))
print()
print("київ.count(50.45) =", київ.count(50.45))
print("київ.index(30.52) =", київ.index(30.52))
assert not hasattr(київ, "append"), "у кортежа не має бути append"
assert not hasattr(київ, "sort"), "і sort теж"
assert hasattr(київ, "count") and hasattr(київ, "index")
print("✅ у кортежа лишились рівно два методи: count та index")
append у списку : True append у кортежа : False sort у кортежа : False count у кортежа : True index у кортежа : True київ.count(50.45) = 1 київ.index(30.52) = 1 ✅ у кортежа лишились рівно два методи: count та index
2 · Кома важливіша за дужки¶
Головна пастка теми. Три майже однакові рядки — і два різні типи.
Зверни увагу: у (5) дужки просто групують вираз, як у математиці.
без_коми = (50.45) # це НЕ кортеж — просто число в дужках
з_комою = (50.45,) # кортеж з одного елемента
без_дужок = 50.45, # теж кортеж: дужки взагалі не потрібні
порожній = () # єдиний виняток: кортеж без коми
print("без_коми =", без_коми, " тип:", type(без_коми).__name__)
print("з_комою =", з_комою, " тип:", type(з_комою).__name__)
print("без_дужок =", без_дужок, " тип:", type(без_дужок).__name__)
print("порожній =", порожній, " тип:", type(порожній).__name__, " довжина:", len(порожній))
assert isinstance(без_коми, float), "(50.45) мало лишитись числом"
assert з_комою == без_дужок, "кому робить кома, а не дужки — обидва мають збігтися"
assert len(з_комою) == 1
print("✅ кортеж робить кома")
без_коми = 50.45 тип: float з_комою = (50.45,) тип: tuple без_дужок = (50.45,) тип: tuple порожній = () тип: tuple довжина: 0 ✅ кортеж робить кома
Як це псує реальний код¶
Зайва кома в кінці рядка не викликає помилки одразу — вона мовчки робить кортеж. Помилка вилізе пізніше й у зовсім іншому місці, тому виглядатиме загадково.
місто = "Київ", # ось вона, випадкова кома в кінці рядка
print("місто =", місто)
print("тип =", type(місто).__name__)
print("а очікували ми звичайний рядок 'Київ'")
місто = ('Київ',)
тип = tuple
а очікували ми звичайний рядок 'Київ'
Тепер спробуємо склеїти це «місто» з текстом — і побачимо traceback. Клітинка нижче має впасти: це і є навчальний матеріал. Прочитай останній рядок повідомлення — він прямо називає обидва типи.
print("Місто: " + місто)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[5], line 1 ----> 1 print("Місто: " + місто) TypeError: can only concatenate str (not "tuple") to str
3 · Що кортеж робити відмовляється¶
Дві різні помилки, і різниця між ними змістовна:
TypeError— операція існує, але для цього типу заборонена;AttributeError— у типу взагалі немає такого методу.
київ[0] = 49.84
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[6], line 1 ----> 1 київ[0] = 49.84 TypeError: 'tuple' object does not support item assignment
київ.append(0.0)
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) Cell In[7], line 1 ----> 1 київ.append(0.0) AttributeError: 'tuple' object has no attribute 'append'
Правильний спосіб «змінити» кортеж — зібрати новий. Старий при цьому лишається цілим, і всі, хто на нього дивиться, нічого не помітять.
київ_вище = (50.50, київ[1]) # новий кортеж із однією новою координатою
print("був :", київ)
print("новий:", київ_вище)
print("id старого:", id(київ))
print("id нового :", id(київ_вище), " <- інший обʼєкт")
assert київ == (50.45, 30.52), "старий кортеж мав лишитись недоторканим"
print("✅ оригінал не постраждав")
був : (50.45, 30.52) новий: (50.5, 30.52) id старого: 139672304993600 id нового : 139672269956416 <- інший обʼєкт ✅ оригінал не постраждав
4 · Пакування й розпакування¶
Пакування — кілька значень збираються в кортеж. Розпакування — кілька імен ліворуч розбирають його по порядку.
одеса = 46.48, 30.73 # пакування: дужок немає, а кортеж є
широта, довгота = одеса # розпакування
print("одеса =", одеса)
print("широта =", широта)
print("довгота =", довгота)
assert (широта, довгота) == одеса, "розпакування має бути зворотним до пакування"
print("✅ пакування й розпакування — дзеркальні дії")
одеса = (46.48, 30.73) широта = 46.48 довгота = 30.73 ✅ пакування й розпакування — дзеркальні дії
Кількості мусять збігтися¶
Якщо імен більше або менше, ніж значень, програма зупиниться — і зупиниться до присвоєння. Жодне імʼя не встигне отримати значення.
широта_1, довгота_1, висота_1 = одеса
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In[10], line 1 ----> 1 широта_1, довгота_1, висота_1 = одеса ValueError: not enough values to unpack (expected 3, got 2)
Зірочка забирає решту¶
Імʼя із зірочкою збирає все, що лишилось, і завжди складає це у список — навіть якщо праворуч був кортеж і навіть якщо лишилось нуль елементів.
запис = ("Одеса", 46.48, 30.73, "порт", "південь")
назва, *решта = запис # перше окремо, все інше — оптом
*початок, останнє = запис # зірочка може стояти й на початку
місто_2, *середина, хвіст = запис # і посередині
print("назва =", назва, " решта =", решта, " тип решти:", type(решта).__name__)
print("початок =", початок, " останнє =", останнє)
print("середина =", середина)
assert isinstance(решта, list), "зірочка завжди дає СПИСОК, а не кортеж"
assert решта == [46.48, 30.73, "порт", "південь"]
print("✅ зірочка зібрала", len(решта), "значення у список")
назва = Одеса решта = [46.48, 30.73, 'порт', 'південь'] тип решти: list початок = ['Одеса', 46.48, 30.73, 'порт'] останнє = південь середина = [46.48, 30.73, 'порт'] ✅ зірочка зібрала 4 значення у список
# зірочка спокійно приймає й порожню решту — це не помилка
одне_значення = ("Київ",)
назва_3, *хвіст_3 = одне_значення
print("назва_3 =", назва_3)
print("хвіст_3 =", хвіст_3, " довжина:", len(хвіст_3))
assert хвіст_3 == [], "порожній список — теж нормальний результат зірочки"
print("✅ нуль елементів — теж законний випадок")
назва_3 = Київ хвіст_3 = [] довжина: 0 ✅ нуль елементів — теж законний випадок
Вкладене розпакування¶
Шаблон ліворуч може повторювати будь-яку форму даних. Дужки навколо (ш, д)
кажуть: «другий елемент теж розклади».
запис_києва = ("Київ", (50.45, 30.52))
назва_4, (широта_4, довгота_4) = запис_києва
print("назва_4 =", назва_4)
print("широта_4 =", широта_4)
print("довгота_4 =", довгота_4)
assert (широта_4, довгота_4) == київ, "внутрішній кортеж мав розкластись у ті самі числа"
print("✅ форма шаблону повторила форму даних")
назва_4 = Київ широта_4 = 50.45 довгота_4 = 30.52 ✅ форма шаблону повторила форму даних
Коли частина значень не потрібна, за домовленістю пишуть підкреслення. Це звичайне імʼя — просто всі розуміють, що воно означає «мені байдуже».
_, широта_5, _ = ("Харків", 49.99, 36.23) # цікавить лише широта
print("широта_5 =", широта_5)
print("а в _ лишилось останнє відкинуте значення:", _)
широта_5 = 49.99 а в _ лишилось останнє відкинуте значення: 36.23
5 · Обмін значень¶
Праворуч від = спершу обчислюється все, і з результату пакується
тимчасовий кортеж. Аж потім імена роздаються зліва направо.
Тому нічого не встигає загубитись.
перше = 1
друге = 2
print("до обміну :", "перше =", перше, " друге =", друге)
перше, друге = друге, перше # правий бік стає кортежем (2, 1)
print("після :", "перше =", перше, " друге =", друге)
assert (перше, друге) == (2, 1), "обмін не спрацював"
print("✅ обмін через кортеж")
до обміну : перше = 1 друге = 2 після : перше = 2 друге = 1 ✅ обмін через кортеж
А тепер наївна спроба «в лоб», без коми. Вона не падає з помилкою — і саме тому небезпечна: код працює, а результат неправильний.
третє = 1
четверте = 2
третє = четверте # одиниця тут-таки втрачена: на неї більше ніхто не дивиться
четверте = третє # копіює двійку саму в себе
print("третє =", третє, " четверте =", четверте)
print("очікували 2 і 1, отримали", третє, "і", четверте)
assert третє == четверте == 2, "наївний спосіб саме так і ламається"
print("⚠️ обміну не сталося — обидва імені осіли на двійці")
третє = 2 четверте = 2 очікували 2 і 1, отримали 2 і 2 ⚠️ обміну не сталося — обидва імені осіли на двійці
6 · Повернення кількох значень¶
Забігання наперед: функції будуть у темі 14. Але механізм уже зрозумілий —
«кілька повернень» це насправді один кортеж, який розпаковують на місці виклику.
Спершу на вбудованій divmod, потім на своїй функції.
результат = divmod(17, 5) # ділення з остачею
print("divmod(17, 5) =", результат, " тип:", type(результат).__name__)
частка, остача = результат # розпаковуємо одразу
print("частка =", частка, " остача =", остача)
assert 5 * частка + остача == 17, "ділення з остачею мало зійтися"
print("✅ 5 ×", частка, "+", остача, "= 17")
divmod(17, 5) = (3, 2) тип: tuple частка = 3 остача = 2 ✅ 5 × 3 + 2 = 17
# def — це тема 14; тут вона потрібна лише щоб показати кортеж на виході
def межі_маршруту(точки):
# розпаковуємо кожну точку окремо: циклів ми ще не проходили
широта_1, _ = точки[0]
широта_2, _ = точки[1]
широта_3, _ = точки[2]
# кома -> функція повертає ОДИН кортеж із двох чисел
return max(широта_1, широта_2, широта_3), min(широта_1, широта_2, широта_3)
маршрут = (київ, львів, одеса)
північ, південь = межі_маршруту(маршрут) # і одразу розпаковуємо
print("маршрут:", маршрут)
print("найпівнічніша широта:", північ)
print("найпівденніша широта:", південь)
assert (північ, південь) == (50.45, 46.48)
print("✅ функція віддала кортеж, ми його розпакували")
маршрут: ((50.45, 30.52), (49.84, 24.03), (46.48, 30.73)) найпівнічніша широта: 50.45 найпівденніша широта: 46.48 ✅ функція віддала кортеж, ми його розпакували
7 · Наша формула проти бібліотечної¶
Найцінніша частина практики: переконатись, що всередині бібліотеки немає магії. Відстань між двома точками на площині — це теорема Піфагора:
відстань = √((ш₁ − ш₂)² + (д₁ − д₂)²)
Порахуємо її руками з розпакованих координат і звіримо з math.dist,
яка приймає кортежі напряму.
import math
широта_а, довгота_а = київ
широта_б, довгота_б = львів
# теорема Піфагора «в лоб», без жодних бібліотек
наша_відстань = math.sqrt((широта_а - широта_б) ** 2 + (довгота_а - довгота_б) ** 2)
бібліотечна_відстань = math.dist(київ, львів)
print("наша формула :", наша_відстань)
print("math.dist :", бібліотечна_відстань)
print("різниця :", abs(наша_відстань - бібліотечна_відстань))
assert math.isclose(наша_відстань, бібліотечна_відстань), "розрахунок розійшовся!"
print("✅ збігається")
наша формула : 6.518604145060503 math.dist : 6.518604145060504 різниця : 8.881784197001252e-16 ✅ збігається
Це відстань у градусах, а не в кілометрах: на сфері так рахувати не можна. Для навчальної задачі згодиться, бо нас цікавить робота з кортежами, а не картографія.
Тепер довжина всього маршруту — сума трьох відрізків. Циклів ми ще не проходили, тому просто випишемо відрізки по одному.
повний_маршрут = (київ, львів, одеса, київ) # виїхали з Києва й повернулись
відрізок_1 = math.dist(повний_маршрут[0], повний_маршрут[1])
відрізок_2 = math.dist(повний_маршрут[1], повний_маршрут[2])
відрізок_3 = math.dist(повний_маршрут[2], повний_маршрут[3])
довжина_маршруту = відрізок_1 + відрізок_2 + відрізок_3
print("Київ -> Львів:", round(відрізок_1, 3))
print("Львів -> Одеса:", round(відрізок_2, 3))
print("Одеса -> Київ :", round(відрізок_3, 3))
print("разом :", round(довжина_маршруту, 3))
# нерівність трикутника: замкнений маршрут не коротший за подвоєну пряму Київ-Львів
assert довжина_маршруту >= 2 * math.dist(київ, львів), "замкнений шлях не може бути коротшим"
print("✅ нерівність трикутника виконується")
Київ -> Львів: 6.519 Львів -> Одеса: 7.495 Одеса -> Київ : 3.976 разом : 17.989 ✅ нерівність трикутника виконується
8 · Незмінність лише на один рівень¶
Кортеж фіксує набір посилань, а не вміст обʼєктів на іншому кінці стрілок. Якщо всередині лежить список — його ніхто не заморожував.
дані = ([1, 2], "текст")
адреса_списку_до = id(дані[0])
дані[0].append(3) # стрілку не чіпаємо — міняємо сам список
print("дані після append :", дані)
print("id(дані[0]) до :", адреса_списку_до)
print("id(дані[0]) після :", id(дані[0]), " <- та сама адреса!")
assert id(дані[0]) == адреса_списку_до, "слот кортежа мав лишитись на тому самому обʼєкті"
assert дані[0] == [1, 2, 3], "а от вміст списку змінився"
print("✅ слот заморожений, обʼєкт у ньому — ні")
дані після append : ([1, 2, 3], 'текст') id(дані[0]) до : 139672270065088 id(дані[0]) після : 139672270065088 <- та сама адреса! ✅ слот заморожений, обʼєкт у ньому — ні
А спроба перевести стрілку на інший обʼєкт падає, як і має. Клітинка нижче має впасти.
дані[0] = [9]
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[22], line 1 ----> 1 дані[0] = [9] TypeError: 'tuple' object does not support item assignment
9 · Хешованість і ключі словника¶
Хеш — короткий числовий «відбиток» обʼєкта. Він має бути сталим, доки обʼєкт лежить ключем. Тому змінювані типи хеша не мають узагалі, а кортеж має його лише тоді, коли хешовані всі його елементи.
міста = {} # словники — тема 08, тут лише як демонстрація
міста[київ] = "Київ"
міста[львів] = "Львів"
міста[одеса] = "Одеса"
print("словник із кортежними ключами:")
print(міста)
print()
print("пошук за парою координат:", міста[(50.45, 30.52)])
print("hash(київ) =", hash(київ))
assert міста[київ] == "Київ", "кортеж має працювати ключем"
print("✅ кортеж — законний ключ словника")
словник із кортежними ключами:
{(50.45, 30.52): 'Київ', (49.84, 24.03): 'Львів', (46.48, 30.73): 'Одеса'}
пошук за парою координат: Київ
hash(київ) = -2200792999891765937
✅ кортеж — законний ключ словника
hash([50.45, 30.52])
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[24], line 1 ----> 1 hash([50.45, 30.52]) TypeError: unhashable type: 'list'
І найтонший випадок: кортеж зовні незмінний, але всередині список.
Повідомлення назве 'list', а не 'tuple' — Python прямо вказує на винуватця.
hash(([1], ))
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[25], line 1 ----> 1 hash(([1], )) TypeError: unhashable type: 'list'
10 · namedtuple: коли поля мають імена¶
запис[2] нічого не каже читачеві. запис.довгота — каже все.
Найважливіше: це справжній кортеж, з усіма його властивостями.
from collections import namedtuple
Точка = namedtuple("Точка", "широта довгота")
київ_іменований = Точка(50.45, 30.52)
print("сам обʼєкт :", київ_іменований)
print("по імені .широта :", київ_іменований.широта)
print("по індексу [0] :", київ_іменований[0])
print("довжина :", len(київ_іменований))
print("це кортеж? :", isinstance(київ_іменований, tuple))
ш, д = київ_іменований # розпакування працює як завжди
print("розпакування :", ш, д)
assert isinstance(київ_іменований, tuple), "namedtuple мусить лишатись кортежем"
assert київ_іменований == київ, "і дорівнювати звичайному кортежу з тими самими числами"
assert hash(київ_іменований) == hash(київ), "і мати той самий хеш"
print("✅ namedtuple — це кортеж з іменами, не новий тип даних")
сам обʼєкт : Точка(широта=50.45, довгота=30.52) по імені .широта : 50.45 по індексу [0] : 50.45 довжина : 2 це кортеж? : True розпакування : 50.45 30.52 ✅ namedtuple — це кортеж з іменами, не новий тип даних
# змінити поле не можна, але можна зробити копію зі зміненим полем
київ_вище_2 = київ_іменований._replace(широта=50.50)
print("оригінал:", київ_іменований)
print("копія :", київ_вище_2)
print("словник :", київ_іменований._asdict())
assert київ_іменований.широта == 50.45, "оригінал мав лишитись недоторканим"
print("✅ _replace робить НОВИЙ обʼєкт, не чіпаючи старий")
оригінал: Точка(широта=50.45, довгота=30.52)
копія : Точка(широта=50.5, довгота=30.52)
словник : {'широта': 50.45, 'довгота': 30.52}
✅ _replace робить НОВИЙ обʼєкт, не чіпаючи старий
Завдання¶
Три рівні. Розгорнуті умови й критерії «зроблено» — у homework.md, тут коротко, щоб можна було спробувати одразу в цьому ж зошиті.
🟢 Рівень 1¶
Додай до маршрут четверте місто — Харків (49.99, 36.23) — і надрукуй
його координати, розпакувавши їх у два імені. Перевір assert-ом, що
кортеж маршруту після цього має чотири елементи, а вихідний кортеж
маршрут лишився з трьома (підказка: + створює новий кортеж).
🟡 Рівень 2¶
Напиши розпакування, яке з запису
("Харків", 49.99, 36.23, "схід", "мільйонник")
дістає назву, пару координат і список усіх решти ознак — одним рядком.
Перевір assert-ом тип кожного отриманого імені.
🔴 Рівень 3¶
Побудуй словник, у якому ключ — кортеж (місто_1, місто_2), а значення —
відстань між ними. Переконайся, що (київ, львів) і (львів, київ) —
це два різні ключі, поясни словами чому, і зроби так, щоб порядок
не мав значення. Підказка: tuple(sorted(...)).