У темі 05 ти навчився працювати з рядком: брати символ за індексом, різати зрізом, склеювати. І запамʼятав головне обмеження — рядок незмінний. Будь-яка «зміна» насправді створює новий рядок. Це зручно й безпечно, поки тобі достатньо тексту. Але щойно треба тримати десяток різних значень і по черзі їх правити, потрібен інший інструмент. Він називається список (list), і саме з нього починається справжнє програмування.
Наскрізний приклад усієї теми — банальний список покупок. Він достатньо простий, щоб не відволікати, і достатньо реальний, щоб пастки в ньому були справжніми.
01 / СтворенняНавіщо потрібен список
Уяви, що треба зберегти три покупки. Без списку виходить так:
Три імені для трьох речей. Додалась четверта — дописуй рядок коду. Скільки всього покупок? Відсортувати? Немає як: три окремі імені нічим між собою не повʼязані, і Python не знає, що це «одне й те саме, тільки різне».
Список розвʼязує це одним махом. Він упорядкований набір значень під одним іменем:
Квадратні дужки — літерал списку, кома розділяє елементи. Функція len()
повертає кількість елементів; вона працює і для рядка (тема 05), і тут — це загальний
спосіб спитати «скільки в тобі всього».
Створити список можна ще двома способами. Порожній: [] або
list(). І перетворенням з іншої послідовності: list("абв")
дасть ['а', 'б', 'в'] — рядок розсипається на окремі символи.
Важлива дрібниця, яка відрізняє Python від багатьох мов: елементи не зобовʼязані бути однакового типу. Список — це просто послідовність чого-завгодно:
Рядок, дробове число, логічне значення й ще один список усередині — ніякої помилки. Зараз зрозуміємо, чому це взагалі можливо.
02 / ПамʼятьЩо насправді лежить у комірках
У темі 03 ми зʼясували: імʼя — це ярлик на обʼєкт, а не коробка зі значенням. Список влаштований так само, тільки ярликів у ньому багато й звуться вони не іменами, а комірками. Комірка з номером 0, комірка з номером 1 і так далі.
Кожна комірка не вміщує елемент — вона вказує на обʼєкт, який живе окремо в памʼяті. Саме тому список може тримати значення різних типів: усі комірки однакові за розміром, бо в кожній лежить не значення, а посилання.
Ця одна фраза пояснює половину теми. Пройди інтерактив крок за кроком і подивись, що відбувається, коли ми присвоюємо щось у комірку.
Інтерактив 1 · Список зберігає посилання
Угорі — сам список. Унизу — обʼєкти, на які вказують його комірки.
покупки[1] = "кефір"
не змінило рядок «молоко» — рядки незмінні, змінити їх неможливо. Змінилась
комірка: тепер вона дивиться на інший обʼєкт. А імʼя дублікат
лишилось на старому й нічого не помітило.int — рівно 40 байтів. Список Python зберігає
підряд вказівники, по 8 байтів кожен, а самі числа розкидані по купі.
Звідси і гнучкість, і ціна: список зі ста мільйонів чисел важить у рази більше за
масив numpy тієї самої довжини, а обхід по ньому стрибає памʼяттю
замість читати її послідовно.03 / ДоступІндекс і зріз
Синтаксис тут точнісінько такий самий, як у рядка, — і це не збіг. Індексація та зрізи в Python описані для послідовностей взагалі, а рядок і список — два різні види послідовності. Вивчивши це один раз на рядках, ти отримав його безкоштовно для списків.
Одна принципова різниця з рядком усе ж є. "текст"[0] повертає рядок
із одного символу — окремого типу «символ» у Python немає. А покупки[0]
віддає сам обʼєкт із комірки: рядок, число, вкладений список — що там було,
те й дістанеш.
Зріз завжди повертає новий список. Це найдешевший спосіб отримати копію — і ми до нього ще повернемось, бо копія ця не така надійна, як здається.
Вихід за межі — помилка, і це добре:
А от зріз за межі мовчить: покупки[2:99] просто віддасть усе, що є,
без жодної скарги. Різницю варто памʼятати: індекс суворий, зріз поблажливий.
04 / ЗмінюваністьГоловна відмінність від рядка
Ось воно — те, заради чого списки взагалі існують. Спробуємо однакову дію з рядком і зі списком:
Повідомлення читається буквально: обʼєкт типу str не підтримує
присвоєння в елемент. Список підтримує. Списки — змінювані
(mutable), рядки — незмінні (immutable), і це головний поділ,
який ми ввели ще в темі 03.
Наслідок глибший, ніж здається. «Змінити» рядок означає створити новий обʼєкт — старий лишається цілим, і всі, хто на нього дивився, нічого не помітять. Змінити список означає перебудувати той самий обʼєкт — і це побачать усі, хто тримає на нього ярлик. Порівняй обидва сценарії поруч:
Інтерактив 2 · Змінюваний проти незмінного
Ліворуч список, праворуч рядок. Дія однакова, результат протилежний.
id не змінився
жодного разу — це весь час один обʼєкт із новим вмістом. У рядка інакше: щоб
отримати «Хліб» замість «хліб», довелось зібрати новий рядок, і номер
став інший.05 / МетодиЩо список уміє робити із собою
Метод — це функція, яка живе всередині обʼєкта й працює саме з ним.
Викликається через крапку: покупки.append("мед"). У рядка методи
(upper(), split()) повертали новий рядок, бо
іншого шляху не було. Методи списку переважно нічого не повертають —
вони змінюють сам список.
Це джерело класичної помилки новачка:
Метод append дописав елемент і повернув None — «нічого».
Це «нічого» ми й поклали в імʼя покупки, втративши список. Правило:
методи, які змінюють список на місці, викликають окремим рядком, без
присвоєння.
Перемикай варіанти в інтерактиві — і особливу увагу зверни на перші три:
різниця між append і extend ловить кожного другого.
Інтерактив 3 · Шість методів наживо
Однаковий вихідний список, різні виклики. Помаранчевим — те, що змінилось.
append додає один елемент —
хай навіть цей елемент сам є списком, і тоді список стає вкладеним.
extend додає кожен елемент переданої послідовності окремо.
Довжина після append([...]) — 4, після extend([...]) — 5.Повний набір, який варто знати напамʼять:
| метод | що робить | повертає |
|---|---|---|
| append(x) | дописує один елемент у кінець | None |
| extend(інша) | дописує в кінець усі елементи іншої послідовності | None |
| insert(i, x) | вставляє x так, щоб він став елементом з номером i | None |
| remove(x) | видаляє перший елемент, рівний x; немає такого — ValueError | None |
| pop(i) | видаляє елемент за номером i (без номера — останній) | сам елемент |
| clear() | прибирає всі елементи, список лишається тим самим обʼєктом | None |
| index(x) | номер першого входження x; немає — ValueError | ціле число |
| count(x) | скільки разів x трапляється у списку | ціле число |
| sort() | сортує список на місці | None |
| reverse() | перевертає порядок на місці | None |
| copy() | поверхнева копія (див. розділ 08) | новий список |
index і count — єдині тут, що нічого не змінюють і
повертають число. Але щоб просто спитати «чи є таке», є коротший оператор:
"мед" in покупки дає True або False і не
падає з помилкою, коли елемента немає.
remove. Він шукає за
значенням, а не за номером, і прибирає лише перше входження. Якщо в списку
два «хліби», покупки.remove("хліб") прибере один — залишиться другий.
А якщо хліба немає взагалі, буде ValueError, і програма зупиниться.
Перед видаленням варто спитати if "хліб" in покупки — про умови
буде тема 11.06 / Зріз ліворучПрисвоєння зрізу
У реальному коді ця можливість трапляється нечасто, але чудово показує, наскільки глибоко зріз вбудований у мову. Його можна поставити ліворуч від знака «=» — і Python замінить цілу ділянку списку на іншу послідовність:
Ми вирізали дві комірки і вставили на їхнє місце три елементи: довжина зросла з
чотирьох до пʼяти, бо при присвоєнні зрізу кількість елементів не
мусить збігатися. Це інша операція, ніж покупки[1] = "сир",
де змінюється рівно одна комірка.
Звідси два трюки: порожня послідовність праворуч видаляє ділянку
(покупки[1:3] = []), а зріз нульової довжини вставляє, нічого не
видаляючи (покупки[2:2] = ["мед"] — те саме, що
insert(2, "мед")). І один справді корисний випадок:
Перший рядок перечіпляє ярлик. Другий випорожнює й наповнює наявний обʼєкт — і якщо на цей список дивиться ще чиєсь імʼя, воно побачить оновлення разом з усіма.
07 / Порядокsort і sorted
Впорядкувати список можна двома способами, і плутанина між ними — друга за
популярністю помилка після append:
покупки.sort()— метод. Сортує список на місці й повертаєNone. Вихідний порядок втрачено назавжди.sorted(покупки)— вбудована функція. Не чіпає оригінал, а повертає новий відсортований список.
Так само влаштована пара для перевертання: покупки.reverse() —
на місці й None, зріз покупки[::-1] — новий список.
Правило вибору: потрібен оригінал далі — бери функцію або зріз; не
потрібен — бери метод, він нічого не копіює.
Обидва способи приймають key — правило, за яким порівнювати. Замість
самих елементів Python порівнює те, що видає key. Наприклад, вбудована
функція len як ключ упорядкує назви за довжиною:
Придивись до результату уважно. Слова однакової довжини стоять не за абеткою — вони йдуть у тому самому порядку, у якому були у вихідному списку. Це не випадковість, а гарантія мови: сортування в Python стабільне (stable). Елементи, що рівні за ключем, ніколи не міняються місцями. Перевір це в інтерактиві — і заразом подивись, що робить із самим списком кожен із варіантів.
Інтерактив 4 · Сортування, ключ і стабільність
Ліворуч вихідний порядок, праворуч результат. Лінії показують, куди поїхав кожен елемент.
key=len лінії всередині однієї
групи довжини йдуть паралельно й жодного разу не перетинаються — це і є
стабільність. У режимі sorted() ліва колонка лишається неторканою:
функція повернула новий список, оригінал не постраждав.sort()
порівнює рядки за номерами символів у Unicode, а не за українською абеткою. Літери
і, ї, є, ґ в таблиці стоять окремим блоком після
всіх інших кириличних — тому «сіль» опиниться після «сир», а «їжа» — узагалі в кінці.
Для показу людині це неправильно: потрібне сортування за локаллю
(locale.strxfrm як ключ) або хоча б key=str.lower, щоб
великі літери не йшли попереду всіх малих. Алгоритм усередині — Timsort,
гібрид сортування вставками й злиттям, придуманий Тімом Пітерсом саме для Python:
на майже впорядкованих даних він працює за лінійний час, у найгіршому випадку — за
O(n·log n), і він стабільний за побудовою.08 / КопіїКопія, якої немає
У темі 03 ми вже ловили цю пастку — тепер розгорнемо її повністю, бо саме зі списками вона трапляється найчастіше.
Ми чіпали b, а змінилось a. Причина відома: b = a
не створює нічого — це другий ярлик на той самий обʼєкт. Щоб отримати справжній
окремий список, копію треба замовити явно, і способів рівно чотири:
Усі чотири роблять одне й те саме — і всі чотири мають одне й те саме обмеження. Вони створюють поверхневу копію (shallow copy): новий список із новими комірками, але комірки заповнені тими самими посиланнями, що й в оригіналі. Обʼєкти, на які вони вказують, не копіюються.
Поки в списку лежать рядки й числа, різниці не видно: вони незмінні, зіпсувати їх усе одно неможливо. Але щойно всередині опиниться інший список — поверхнева копія протікає. Ось три способи скопіювати вкладений список і що з ними стається:
Інтерактив 5 · Де протікає поверхнева копія
Оригінал — список із двох списків. Обери спосіб копіювання й зроби дві зміни.
У лістингу deepcopy — скорочено від copy.deepcopy.
deepcopy витримує обидва кроки.Ліки — глибока копія (deep copy). Функція
copy.deepcopy(a) обходить структуру рекурсивно і створює новий обʼєкт
на кожному рівні вкладеності:
Здавалося б, бери завжди deepcopy — і не думай. Ні: вона значно
дорожча, бо обходить усе дерево обʼєктів і веде облік уже скопійованого, щоб не
зациклитись. Правило таке: у списку лише незмінні елементи — вистачить
a.copy(); є вкладені змінювані — потрібен deepcopy.
09 / ВкладеніСписки всередині списків
Список у комірці списку — звичайна річ. Найчастіше так представляють таблицю: зовнішній список — рядки, внутрішні — клітинки рядка.
Подвійні дужки читаються зліва направо: спершу чек[1] дає внутрішній
список, потім [2] береться вже від нього.
А тепер найкоротший спосіб зіпсувати собі вечір. Оператор * повторює
список — [0] * 3 дає [0, 0, 0], зручно. Спробуємо так само
зробити таблицю 3×3:
Це та сама поверхнева копія, тільки в профіль. Внутрішній [0] * 3
обчислився один раз. Зовнішнє множення не копіювало його — воно
поклало в три комірки три посилання на цей самий обʼєкт. Три рядки таблиці — це
один рядок, показаний тричі.
поле = [[0] * 3 for _ in range(3)]. Тут вираз
[0] * 3 обчислюється заново на кожен обхід, тому й списки виходять
різні. Конструкція for усередині дужок — це тема 13,
а самі цикли — тема 12; зараз просто запамʼятай рецепт і причину,
чому множення тут не працює.10 / УсерединіДинамічний масив і ціна операцій
Цей розділ для тих, кому мало «працює — і добре». Список Python — це динамічний масив (dynamic array): суцільний шматок памʼяті з вказівниками підряд, який уміє рости. Суцільність дає головну перевагу — доступ за індексом коштує однаково, чи то нульовий елемент, чи мільйонний: адреса рахується арифметикою. Мовою складності це O(1), «стала кількість дій незалежно від довжини».
Але суцільність має й ціну. Щоб вставити елемент на початок, треба фізично зсунути всі інші на одну комірку вправо — місця посередині просто немає. Покрути повзунок і подивись, як росте ця робота:
Інтерактив 6 · Ціна append проти insert(0, x)
Помаранчевим позначено комірки, які доводиться фізично зсунути.
Лишається питання, чому append дешевий. Масив же суцільний: коли
місце скінчиться, доведеться просити в системи більший шматок памʼяті й
переносити туди все. Хіба це не дорого?
Дорого — але рідко. Список ніколи не виділяє памʼять по одній комірці. Коли місце закінчується, він просить із запасом, приблизно на восьму частину більше, ніж потрібно зараз. У CPython це буквально одна формула:
Тут n — потрібна нова довжина, n // 8 — той самий запас
у 12,5%, а + 6 дає невеликим спискам мінімальний буфер, щоб перші
кілька append точно не викликали перевиділення. Підстав n = 1:
1 + 0 + 6 = 7, униз до кратного чотирьох — 4. Список
із одного елемента одразу має чотири комірки, три з них порожні. Покрути повзунок
і подивись, як ця формула працює далі:
Інтерактив 7 · Як список росте блоками
Суцільні комірки — зайняті, пунктирні — виділені про запас. Унизу — крива ємності.
append просто кладе значення у вже готову
комірку. Оскільки запас пропорційний розміру, перевиділення стають дедалі
рідшими, і середня ціна одного append лишається сталою.Це називають амортизованою складністю O(1): окремий
append зрідка буває дорогим, але в середньому по багатьох викликах
ціна стала. А от insert(0, x) ніякої амортизації не має — він чесно
платить O(n) щоразу.
Коли додавати й забирати треба з обох кінців, списку варто пошукати
заміну. У стандартній бібліотеці є collections.deque — двобічна черга
на звʼязаних блоках. У неї appendleft і popleft коштують
O(1) замість O(n):
Плата — втрачений швидкий доступ до середини: черга[5000] у
deque вже не константа. Виграв на кінцях — програв у середині.
| операція | складність | чому саме так |
|---|---|---|
| покупки[i] | O(1) | адреса рахується арифметикою |
| append(x) | O(1) аморт. | місце майже завжди вже виділено |
| pop() | O(1) | з кінця — нічого зсувати |
| insert(0, x) | O(n) | усі елементи їдуть на комірку вправо |
| pop(0) | O(n) | усі елементи їдуть на комірку вліво |
| remove(x) | O(n) | спершу пошук, потім зсув |
| x in покупки | O(n) | перебір, поки не знайдеться |
| sort() | O(n·log n) | Timsort |
Останній рядок варто виділити окремо. Перевірка x in покупки —
лінійна: Python перебирає елементи, поки не натрапить на потрібний.
На списку з мільйона рядків це помітно повільно. Для частих перевірок «чи є таке»
існує множина (тема 09), де ця сама операція коштує O(1).
11 / ПідсумокЩо забрати з теми
Список — упорядкована змінювана послідовність посилань. З цього одного речення випливає все, що ми розібрали:
- Комірки зберігають посилання, а не значення. Тому в списку можуть лежати різнотипні елементи, і тому вкладений список — один обʼєкт, на який можна дивитися з кількох місць.
- Список змінюється на місці.
покупки[0] = "булка"працює там, де рядок кидаєTypeError. Обʼєкт лишається той самий — зміну бачать усі імена, що на нього вказують. appendдодає один елемент,extend— кожен окремо. Обидва повертаютьNone, тому не пишипокупки = покупки.append(...).sort()псує оригінал,sorted()— ні. Те саме дляreverse()проти[::-1]. Сортування стабільне: рівні за ключем елементи не міняються місцями.b = a— не копія.a[:],list(a)йa.copy()— копія поверхнева, і вона протікає на вкладених списках. Глибока — тількиcopy.deepcopy.- Кінець списку дешевий, початок дорогий.
append— O(1) амортизовано,insert(0, x)— O(n). Треба з обох кінців — бериdeque.
У темі 07 ми візьмемо найближчого родича списку — кортеж (tuple). Виглядає майже так само, тільки в круглих дужках, а поводиться протилежно: змінити його неможливо. І питання буде саме те, яке ти вже вмієш ставити: навіщо комусь навмисно незмінна версія списку? Відповідь повʼязана і з безпекою від випадкових змін, і зі швидкістю, і зі словниками з теми 08.
practice.ipynb ти зібереш
робочий список покупок з нуля, зловиш пастку копії власними руками — і перевіриш
кожен свій висновок через assert, який мовчить, коли все правильно,
і кричить, коли ні.Далі в темі
Теорію прочитано. Тепер закріпи її на практиці.