У темі 06 ми навчилися складати багато значень в один список і крутити його як завгодно: додати, вставити, видалити, відсортувати. Тепер візьмемо структуру, яка з вигляду майже така сама — теж набір значень по порядку, теж доступ за індексом, — але не дозволяє змінити себе жодним рухом. Це кортеж (tuple), і незручність тут не вада, а весь сенс.
Наскрізний приклад теми — координати точок на карті. Пара «широта, довгота» — це рівно те, що ніколи не має мінятися частинами: точка або переїхала цілком, або стоїть на місці. Половина точки, у якої вже нова широта, але ще стара довгота, — це не точка, а баг.
01 / ЗнайомствоЩо таке кортеж
Кортеж створюють, перелічивши значення через кому. Дужки навколо ставлять для читабельності:
Усе, що ти вмів робити зі списком на читання, працює й тут — і працює однаково:
А от усе, що змінює, зникло. Немає ані append, ані
insert, ані remove, ані sort. Немає й
присвоєння за індексом:
Зверни увагу на дві різні помилки. Перша каже «цей тип не підтримує присвоєння за елементом», друга — «у цього типу взагалі немає такого методу». Перша — про те, що операція заборонена; друга — про те, що її не існує. Читати повідомлення про помилку варто до кінця: воно майже завжди називає і тип, і дію.
З методів у кортежа лишилося рівно два, і обидва тільки читають:
точка.count(50.45) порахує, скільки разів значення трапляється, а
точка.index(30.52) скаже, під яким індексом воно лежить. Два методи
проти сорока в списку — це і є вся різниця в API.
Зробити кортеж зі списку (і навпаки) можна перетворенням:
02 / СинтаксисКому вирішує кома
Тепер найпоширеніша пастка теми. Здається, що кортеж роблять круглі дужки. Це неправда. Дужки в Python — це насамперед групування виразу, те саме, що в математиці. Кортеж робить кома.
Звідси наслідок, на якому спотикаються всі: (5) — це просто число
пʼять у дужках, як (2 + 3). Щоб отримати кортеж із одного елемента,
потрібна кома: (5,). І навпаки, кома працює й без дужок:
5, — уже кортеж.
Порухай повзунок і подивись на тип праворуч. Вираз змінюється мінімально — на один символ, — а обʼєкт виходить принципово різний.
Інтерактив 1 · Кома проти дужок
Шість майже однакових рядків. Кома підсвічена рожевим — саме вона все й вирішує.
(5) і (5,) відрізняються одним
символом, а типи в них різні — int і tuple. Порожній кортеж
() — єдиний виняток: там коми немає, бо й ставити її нема між чим.
Зверни увагу на останнє положення: tuple([5]) теж дає
(5,), але вже без жодної коми — там працює функція, а не синтаксис.місто = "Київ",. Далі print("Місто: " + місто) падає з
TypeError: can only concatenate str (not "tuple") to str, і виглядає це
так, ніби зламався print. Якщо повідомлення про помилку згадує
tuple там, де ти кортежа не писав, — шукай зайву кому.03 / ВибірКортеж чи список
Найчастіша відповідь в інтернеті — «кортеж швидший». Це напівправда, і жити з нею шкідливо. Розберімося чесно: що справді відрізняється, а що ні.
Спершу подивись, які операції з обох боків працюють однаково, а де кортеж відмовляє — і де, навпаки, відмовляє список.
Інтерактив 2 · Одна операція, дві структури
Ліворуч список = [1, 2, 3], праворуч кортеж = (1, 2, 3). Повзунок обирає дію.
hash(), падає навпаки —
у списку. Саме ця нижня сходинка й буде головною причиною брати кортеж.Тепер про швидкість і памʼять — з числами, а не з чутками. Заміри нижче зроблені на CPython 3.12; на іншій машині абсолютні значення будуть інші, а співвідношення — такі самі.
| що міряємо | список | кортеж | висновок |
|---|---|---|---|
| створити літерал 1 млн разів | 0.057 с | 0.007 с | кортеж швидший у 8 разів — але тільки тому, що літерал зі сталих компілятор зберігає в коді готовим, а список щоразу будує заново |
| обійти 1000 елементів | 0.139 с | 0.153 с | різниці немає, вона в межах шуму вимірювання |
| памʼять, 3 елементи | 88 байтів | 64 байти | список тримає запас під майбутні append |
| памʼять, 1000 елементів | 8056 байтів | 8040 байтів | 0.2% — на великих даних економія зникає |
Отже, «швидший» стосується одного вузького випадку: створення дрібного набору
зі сталих значень. У циклі, який мільйон разів робить (0, 0), це
відчутно. У всьому іншому ставити кортеж заради швидкості немає сенсу.
Справжні причини обрати кортеж інші, і їх три:
- Контракт незмінності. Якщо ти передав кортеж у чужий код, ти точно знаєш: він повернеться таким самим. Зі списком такої гарантії немає — згадай тему 03: передається не копія, а ярлик на той самий обʼєкт.
- Хешованість. Кортеж можна покласти в ключ словника, список — ні. Це вже не про зручність, а про те, що можливо, а що ні. Розділ 08.
- Змістова різниця. Список — це «багато однорідних штук, кількість плаває»: покупки, оцінки, відвідувачі. Кортеж — це «одна річ, у якої кілька різних полів, і кількість полів фіксована»: координата, дата, розмір зображення. Читач коду розуміє задум ще до того, як дійде до логіки.
Практичне правило: беручи список, ти обіцяєш, що кількість елементів може змінитися; беручи кортеж — що ні.
04 / РозпакуванняПакування й розпакування
Тут кортеж перестає бути «списком, який шкода» і стає окремим інструментом.
Пакування (packing) — коли кілька значень збираються в один
кортеж. Це вже було: точка = 50.45, 30.52. Розпакування
(unpacking) — зворотна дія: кілька імен зліва отримують елементи по порядку.
Ліворуч не список імен і не якась спецконструкція — це той самий синтаксис коми.
Python бачить кому зліва від = і розуміє: праворуч має бути щось, що
розкладається на частини.
Головне обмеження — кількості мусять збігтися. Інакше програма зупиниться, і зупиниться до присвоєння: жодне імʼя не встигне отримати значення. Це важлива деталь — після невдалого розпакування в тебе не буде наполовину заповнених імен.
А якщо частину значень треба взяти «оптом», ставлять зірочку. Імʼя із зірочкою забирає все, що лишилось, і завжди складає це у список — навіть якщо праворуч був кортеж, навіть якщо лишилось нуль елементів.
Прокрути повзунок через усі вісім шаблонів. Три з них помилкові — подивись, що саме Python скаже й чому.
Інтерактив 3 · Що куди потрапляє
Угорі — дані праворуч від =. Унизу — імена ліворуч. Стрілки показують призначення.
Ще два правила про зірочку. По-перше, вона може бути лише одна: із двома
a, *x, *y = … Python не знав би, де закінчується перша група, і
відмовляється ще на етапі читання коду —
SyntaxError: multiple starred expressions in assignment. По-друге,
зірочка працює й посередині: перший, *середина, останній = дані.
divmod, яка ділить із остачею:
05 / ОбмінОбмін значень без тимчасової змінної
У темі 03 ми вже бачили
рядок a, b = b, a й назвали його «магічним обміном». Тепер, коли ти
знаєш про пакування, магії не лишається — розберемо механіку по кроках.
Python виконує присвоєння у два етапи, і саме порядок етапів усе рятує:
- Спершу повністю обчислюється права частина. Вирази
bіaдають свої поточні значення, з них пакується тимчасовий кортеж(2, 1). Імена в цей момент ще не чіпали. - Потім кортеж розпаковується зліва направо. Перше імʼя отримує перший елемент, друге — другий.
Оскільки старі значення вже лежать усередині кортежа, перезапис імені нічого не псує. Наївний спосіб «у лоб» ламається саме тому, що в нього такого проміжного сховища немає. Порівняй обидва режими покроково:
Інтерактив 4 · Куди дівається одиниця
Угорі — рядок коду з підсвіченою частиною, яку Python обробляє зараз. Посередині — тимчасовий кортеж.
(2, 1) притримав
обидва значення, поки імена перевʼязувались, — вийшло a = 2,
b = 1. Праворуч притримувати не було чим: після
a = b на одиницю не дивиться вже ніхто, і другий рядок лише
копіює двійку сам у себе.BUILD_TUPLE + UNPACK_SEQUENCE компілятор ставить
інструкцію SWAP прямо на стеку, і жодного кортежа в памʼяті не
виникає. Побачити це можна через dis.dis("a, b = b, a"). На поведінку
це не впливає ніяк — але пояснює, чому обмін через кортеж не повільніший за обмін
через третю змінну, а трохи швидший.06 / ВкладеністьВкладене розпакування
Шаблон ліворуч може повторювати будь-яку форму даних, а не лише плоский ряд. Якщо праворуч кортеж, усередині якого ще кортеж, — ліворуч так само пишуть кортеж усередині кортежа:
Дужки ліворуч тут обовʼязкові: саме вони кажуть «другий елемент теж розкласти».
Без них назва, широта, довгота = запис дасть
ValueError: not enough values to unpack (expected 3, got 2) — бо
верхній рівень має рівно два елементи. Останнє положення повзунка в інтерактиві 3
показує цей випадок цілком.
Розпакувати можна не тільки кортеж, а будь-що ітероване: список, рядок, а згодом — і результат роботи циклу:
Коли частина значень не потрібна, за домовленістю пишуть підкреслення. Це звичайне імʼя, ніякої особливої сили в ньому немає — але кожен, хто читає код, розуміє: «сюди щось попало, і мені воно нецікаве».
07 / МежаНезмінність лише на один рівень
Тепер найтонше місце теми, і його варто зрозуміти дослівно. Коли кажуть «кортеж незмінний», мають на увазі: набір посилань усередині нього зафіксовано назавжди. Слот номер 0 до кінця життя кортежа вказує на той самий обʼєкт.
Але це нічого не говорить про сам обʼєкт на іншому кінці стрілки. Якщо там лежить список — його ніхто не заморожував:
Обидва рядки виглядають як «зміна кортежа», але роблять різні речі. Перший намагається перевести стрілку на інший обʼєкт — заборонено. Другий стрілку не чіпає взагалі, він змінює обʼєкт, на який вона вказує, — а це вже не справа кортежа.
Інтерактив 5 · Стрілки заморожені, обʼєкти — ні
Кортеж дані = ([1, 2], "текст"). Спробуй чотири дії й подивись, які з них проходять.
id(дані[0]) не змінюється ніколи —
слот справді заморожений. Але вміст списку за цією адресою після
append уже інший. Тому й hash(дані) не працює:
відбиток такого кортежа не був би сталим.08 / ХешХешованість і чому кортеж стає ключем
Ми двічі згадали hash() — час пояснити, що це і навіщо.
Хеш — це коротке ціле число, «відбиток» обʼєкта. Обчислюється він швидко й має одну ключову властивість: рівні обʼєкти дають рівний хеш. Завдяки цьому Python може не порівнювати вміст із кожним кандидатом по черзі, а одразу порахувати число й піти за ним, як за номером комірки в камері схову.
Саме на цьому побудовані словники (тема 08) і множини (тема 09). І звідси — жорстка вимога: поки обʼєкт лежить ключем, його відбиток не має змінюватись. Інакше річ, яку поклали в комірку 17, після зміни «мала б» лежати в комірці 903 — і знайти її стане неможливо.
Тому змінювані типи хеш просто не мають. Це не забудькуватість авторів мови,
а свідома заборона: hash([1, 2]) дає
TypeError: unhashable type: 'list'.
Кортеж незмінний — отже, здавалося б, хешований завжди. Але ні. Хеш кортежа рахується з хешів його елементів: Python бере відбиток кожного елемента й перемішує їх в одне число. Якщо хоч один елемент відбитка не має, рахувати нема з чого:
Зверни увагу на повідомлення: воно каже 'list', а не
'tuple'. Python чесно називає винуватця — той елемент усередині,
на якому все зламалось. Перевір своє чуття на шести обʼєктах:
Інтерактив 6 · Що можна класти в ключ
Спершу вгадай сам, потім тисни «Перевірити». Зелена галочка — елемент хешований.
Практична користь від цього величезна. Ключем словника може бути пара координат, пара «місто-рік», трійка «рік-місяць-день» — будь-який складений ключ. Зі списками таке неможливо взагалі:
09 / Імена полівКоли [0] уже незручно
У кортежа є одна справжня вада: звертання за номером. запис[2] нічого
не каже читачеві. Через тиждень доведеться шукати, що ж там лежало під двійкою.
Для цього в стандартній бібліотеці є namedtuple — «кортеж з іменами
полів». Це не окремий тип даних, а спосіб швидко зробити свій різновид кортежа:
Найважливіше: це справжній кортеж. Він незмінний, хешований,
isinstance(київ, tuple) дає True, і будь-який код, що
чекає кортеж, прийме його без питань. Просто до індексів додались імена.
Змінити поле не можна — але можна зробити копію зі зміненим полем, і метод для цього названий з підкресленням, щоб не зіткнутися з іменем твого поля:
Правило вибору просте: два-три елементи, зміст яких зрозумілий із контексту, —
звичайний кортеж. Чотири й більше, або поля, які легко переплутати місцями, —
namedtuple. А коли знадобляться ще й методи та перевірки, у темі 28
візьмемо @dataclass(frozen=True), який робить те саме, але потужніше.
10 / ПідсумокЩо забрати з теми
- Кортеж робить кома, не дужки.
(5)— число,(5,)— кортеж,()— порожній кортеж-виняток. - Кортеж — це список без усього, що змінює. Читання однакове,
методів лишилось два:
countіindex. - Обирай не за швидкістю. Різниця є лише на створенні дрібних літералів. Обирай за змістом: плаваюча кількість однорідних штук — список, фіксований набір полів — кортеж.
- Розпакування — головна суперсила. Кількості мусять збігтися, зірочка забирає решту в список, вкладеність повторює форму даних.
- Обмін працює, бо правий бік обчислюється цілком і пакується в тимчасовий кортеж до того, як хоч одне імʼя перевʼяжеться.
- Незмінність — на один рівень. Слоти заморожені, обʼєкти всередині — ні.
- Хешований лише кортеж із хешованих елементів. Це і є перепустка в ключі словника.
Ми щойно двічі вперлися в те, чого ще не знаємо: у комірки, за якими Python знаходить значення за ключем. Це тема 08 — словники. Там ми розберемо, як хеш перетворюється на номер комірки, чому пошук за ключем не залежить від розміру словника і чому ключем може бути кортеж, але ніколи не список. Усе, що ти дізнався про хешованість, там знадобиться з першого абзацу.
practice.ipynb ти
збереш маршрут із координат-кортежів, розпакуєш дані трьома різними шаблонами,
власноруч зловиш TypeError на кортежі зі списком усередині й
перевіриш через assert, що твій розрахунок відстані збігається з
math.dist.Далі в темі
Теорію прочитано. Тепер закріпи її на практиці.