Python з нуля · Блок 1 · Тема 05

Рядок, який не можна змінити

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

У темі 03 ми домовились, що ім'я — це ярлик, наліплений на об'єкт, і що об'єкти бувають незмінні та змінювані. У темі 04 виявилось, що числа належать до першої групи: не можна «зробити з п'ятірки шістку», можна лише перевісити ярлик на іншу п'ятірку чи шістку. Тепер черга тексту. І тут та сама історія повторюється з несподіваною силою: рядок теж не змінюється ніколи, а кожна дія над ним створює новий об'єкт.

Наскрізний приклад у нас буде один — рядок із даними, який міг би прилетіти з файла чи з мережі:

запис = "Київ;кава;249.50"

Шістнадцять символів. З них ми навчимося витягати місто, назву товару й ціну, порахуємо, скільки це в байтах (спойлер: не шістнадцять), складемо з них охайний звіт — і побачимо щонайменше три способи зробити це неправильно.

01 / ЛапкиЯк народжується рядок

Рядок (string) — це літерал у лапках. Лапки бувають одинарні й подвійні, і різниці між ними немає жодної: 'кава' і "кава" дають однаковий об'єкт. Два варіанти існують рівно з однієї причини — щоб покласти всередину лапки іншого виду без зайвої мороки:

"він сказав 'ні'"    'ціна — 249.50 грн, тип "б"'

Якщо всередині потрібні саме такі лапки, якими рядок відкрито, їх екранують (escape) зворотним слешем: 'він сказав \'ні\''. Зворотний слеш вмикає спеціальне значення наступного символу — і саме тому сам слеш теж доводиться подвоювати.

записщо означаєде трапляється
\nперехід на новий рядоктекстові файли, друк у кілька рядків
\tтабуляціявирівнювання колонок у консолі
\\сам зворотний слешшляхи Windows, регулярні вирази
\' \"лапка всередині таких самих лапокцитати
\u0457символ за його кодом Unicodeколи символу немає на клавіатурі

Третій спосіб — потрійні лапки """…""". Усередині них можна вільно тиснути Enter: усі переходи на новий рядок збережуться в самому рядку. Це основний спосіб записати багаторядковий текст — і водночас те, чим пишуть документацію до функцій (побачимо в темі 14).

І четвертий, який рятує від війни зі слешами, — сирий рядок (raw string): літера r перед лапками вимикає обробку екранувань. r"C:\новий\текст" залишиться саме таким, як написано, а без r Python побачив би там \n і \t і мовчки зіпсував шлях. Регулярні вирази (тема 23) пишуть виключно так.

Дрібниця, яка колись зіпсує тобі вечір. Два літерали, що стоять поруч, Python склеює автоматично: "Ки" "їв" — це "Київ", без жодного плюса. Зручно для довгих текстів, розбитих на кілька рядків коду. Але якщо десь у переліку рядків забути кому, замість двох елементів мовчки з'явиться один склеєний — і помилка не покажеться доти, доки хтось не перерахує елементи.

02 / ПослідовністьРядок — це послідовність символів

Рядок не є атомарною «одиницею тексту». Це впорядкована послідовність символів, у якої кожен має свій номер. Довжину дає вбудована функція len: len("Київ;кава;249.50") — це 16.

Нумерація починається з нуля. Перший символ — запис[0], останній — запис[15]. Здається сваволею, але за цим стоїть проста ідея: індекс — це не «котрий за рахунком», а відстань від початку. Нульовий символ стоїть на нульовій відстані від початку. З цієї ж думки далі природно виростуть зрізи.

Щоб не рахувати від кінця вручну, є від'ємні індекси: запис[-1] — останній символ, запис[-2] — передостанній. Правило перекладу просте: -k означає len(рядок) - k.

Вийти за межі не можна: запис[16] зупинить програму з IndexError: string index out of range. Це навмисно — краще гучна помилка одразу, ніж тихе сміття через сто рядків.

03 / ЗрізиЗрізи: взяти шматок

Найпотужніший інструмент цієї теми. Зріз (slice) бере не один символ, а цілий діапазон, і записується трьома числами через двокрапку:

рядок[ start : stop : step ]

Читається так: почни з start, зупинись перед stop, крокуй по step символів. Головна дивина тут — що stop не входить у результат. І це не примха, а рішення, яке дає дві дуже зручні властивості:

  1. довжина зрізу = stop − start. запис[0:4] — це рівно чотири символи, рахувати нічого не треба;
  2. шматки стикуються без дірок і без нахлесту. запис[:5] + запис[5:] дорівнює вихідному рядку — для будь-якого місця розрізу.

Будь-яке з трьох чисел можна пропустити, і тоді Python підставить розумне значення: start — початок, stop — кінець, step — одиницю. Тому запис[:4] — це «перші чотири», запис[-6:] — «останні шість», а запис[:] — увесь рядок цілком.

Окремо варто запам'ятати ідіому рядок[::-1]: від'ємний крок розвертає обхід, і ти отримуєш рядок навпаки. Покрути повзунки й подивись, як усе це виглядає на нашому записі.

Інтерактив 1 · Конструктор зрізу

Три повзунки — три числа зрізу. Під рядком два ряди чисел: додатні індекси й від'ємні. Стрілки показують порядок обходу.

вираз
довжина
результат
Два спостереження: символ під міткою stop ніколи не підсвічується — межа виключна. І зріз ніколи не падає: постав stop = 18 при довжині 16, і Python просто віддасть те, що є, без IndexError. Це принципова відмінність зрізу від індексу — і тому зрізом безпечніше брати «перші п'ять символів» із рядка, довжини якого ти не знаєш.
Для тих, хто прийшов з інших мов. запис[:] для рядка повертає той самий об'єкт, а не копію: запис[:] is запис дає True. Копіювати незмінний об'єкт немає сенсу, тому Python цього й не робить. Для списків (тема 06) той самий запис уже створить справжню копію — саме тому, що список змінюваний.

04 / НезмінністьРядок не змінюється ніколи

А тепер головне речення теми. Спробуй виправити першу літеру:

запис[0] = "Л"

Python відповість TypeError: 'str' object does not support item assignment. Не «помилка доступу», не «немає прав» — тип не підтримує таку операцію. Рядок незмінний (immutable), і жодного способу відредагувати його на місці не існує.

Звідси головний наслідок, який визначає весь стиль роботи з текстом: кожна «зміна» рядка — це насправді створення нового рядка. Методи .upper(), .replace(), .strip() нічого не правлять — вони повертають новий об'єкт, а старий лишається як був. Найчастіша помилка новачка виглядає рівно так:

напій = "кава"
напій.upper() # новий рядок створено... і викинуто
print(напій) # кава — бо ми нічого нікуди не записали

Правильно — перевісити ярлик: напій = напій.upper(). Порівняй усі три випадки в інтерактиві, дивлячись на id об'єктів до і після.

Інтерактив 2 · Що насправді відбувається при «зміні»

Три спроби змінити рядок. Стеж за ярликом і за id об'єктів.

id до
id після
напій зараз
нових обʼєктів
Ключ до картинки: у першому випадку нового об'єкта немає взагалі — Python відмовився ще до дії. У другому новий об'єкт створено, і ярлик напій перевішено на нього; старий лишився без жодного ярлика й буде прибраний. У третьому новий об'єкт теж створено — але ярлик на нього не поставили, тож він зникає в ту саму мить.

Навіщо взагалі така незручність

Незмінність — не покарання, а плата за три конкретні речі, кожна з яких коштує дорого:

05 / МетодиЧистимо, міняємо, шукаємо

Метод — це функція, «приклеєна» до об'єкта: пишеться через крапку після нього. Усі методи рядка повертають щось нове й ніколи не чіпають оригінал. Ось той мінімум, який покриває дев'яносто відсотків щоденної роботи.

Регістр

.upper() — усе великими, .lower() — усе малими, .title() — кожне слово з великої, .capitalize() — тільки перша літера всього рядка. Звучить нудно, поки не згадаєш, що регістр — це властивість мови, а не байтів: "ß".upper() дає "SS", тобто довжина рядка після зміни регістру може змінитись. А для порівняння без урахування регістру правильний метод — не .lower(), а .casefold(): він агресивніший і саме для цього призначений.

Прибирання країв

.strip() без аргументів знімає пробіли, табуляції й переходи рядка з обох кінців — перша дія над будь-яким рядком, прочитаним із файла чи від користувача. .lstrip() і .rstrip() роблять те саме з одного боку.

Пастка з аргументом. .strip("абв") знімає не підрядок «абв», а будь-які символи з цього набору, поки вони трапляються скраю. Тому "кава".strip("ак") дасть "в", а не "кава": злетіли «к» і «а» зліва та «а» справа. Якщо треба прибрати саме префікс чи суфікс, є точні методи .removeprefix() і .removesuffix() (Python 3.9+).

Заміна

.replace(старе, нове) замінює усі входження. Третім аргументом можна обмежити кількість: .replace(";", " · ", 1) замінить лише перше. Якщо старого підрядка немає, метод спокійно поверне копію без змін — помилки не буде.

Пошук

.find(що) повертає індекс першого входження або -1, якщо не знайшло. .rfind() шукає з кінця, .count() рахує входження, .startswith() і .endswith() перевіряють початок і кінець. А найпростіше питання «чи є там таке взагалі» ставлять оператором in: "кава" in запис.

Інтерактив 3 · Пошук підрядка

Усі входження підсвічено бірюзовим, перше — рожевим. Саме його індекс повертає find.

будь-який підрядок

find
rfind
count
x in запис
Небезпека, схована в -1: це не «нічого», це цілком валідний індекс — останній символ. Тому перевірка if запис.find(x): хибна двічі: вона пропустить знахідку на позиції 0 і прийме −1 за успіх. Пиши if запис.find(x) >= 0:, а ще краще — if x in запис:.

06 / РозбірРозібрати й зібрати

Дві операції, які зустрічаються в реальному коді частіше за всі інші разом узяті.

.split(роздільник) ріже рядок на частини. .join(частини) склеює їх назад. Разом вони утворюють точну пару: розібрав, полагодив, зібрав.

"Київ;кава;249.50".split(";")  →  ['Київ', 'кава', '249.50']

Три тонкощі, на яких спотикаються всі.

  1. .split() без аргументу — це інша операція. Вона ріже по будь-якій кількості будь-яких пробільних символів і викидає порожні шматки з країв. Тому "a b ".split() дає два елементи, а "a b ".split(" ") — аж чотири, два з яких порожні;
  2. роздільник не «зникає» частково. Якщо він трапився двічі поспіль, між ними народиться порожній рядок. Це не баг: інакше не можна було б відрізнити «поле порожнє» від «поля немає»;
  3. join — метод роздільника, а не списку. Пишеться навпаки до інтуїції: ";".join(частини). Логіка тут така: клеїти можна що завгодно, а от чим клеїти — це і є рядок, тому метод його.

Інтерактив 4 · split і join у обидва боки

Той самий запис, але «брудний»: із зайвими пробілами по краях і навколо роздільників.

клей для join

частин
порожніх
довжина join
Спробуй обов'язково: перемкни на ";" без .strip() — і подивись на пробіли, що прилипли до кожної частини. Саме через них потім "кава" == " кава " дає False, і людина півгодини шукає помилку в даних. Далі перемкни на «без арг.» — побачиш, що .split() вважає окремим словом навіть самотню крапку з комою.
join приймає лише рядки. ";".join(["кава", 3]) впаде з TypeError: sequence item 1: expected str instance, int found. Числа доведеться перетворити явно — і це навмисна суворість: Python не вгадує, як саме ти хотів надрукувати число.

07 / Форматf-рядки й міні-мова формату

Складати текст із плюсів — виснажливо й нечитабельно. Сучасний спосіб один: f-рядок (formatted string literal). Літера f перед лапками вмикає підстановку виразів у фігурних дужках:

місто, товар, ціна = "Київ", "кава", 249.5
print(f"{місто}: {товар} коштує {ціна} грн")
Київ: кава коштує 249.5 грн

Усередині дужок може стояти будь-який вираз, не лише ім'я: f"разом {ціна * 3} грн" працює. Два спеціальні прийоми варто знати одразу: f"{ціна!r}" друкує технічне представлення (repr) замість людського, а f"{ціна=}" друкує одразу й вираз, і значення — ціна=249.5. Другий — найшвидший спосіб налагодження, який взагалі існує в Python.

Двокрапка вмикає міні-мову

Після виразу можна поставити двокрапку й описати, як саме його надрукувати. Загальна форма специфікації така:

[заповнювач][< > ^][знак][ширина][,][.точність][тип]
шматокщо робитьприкладрезультат
>10ширина 10, притиснути праворучf"{249.5:>10}"'     249.5'
<10ширина 10, притиснути ліворучf"{'кава':<10}"'кава      '
^10ширина 10, по центруf"{'кава':^10}"'   кава   '
.2fдва знаки після комиf"{249.5:.2f}"'249.50'
,роздільник тисячf"{1234567:,}"'1,234,567'
+знак навіть у додатнихf"{249.5:+.1f}"'+249.5'
08.3fдоповнити нулями до ширини 8f"{42:08.3f}"'0042.000'
.1%відсоткиf"{0.075:.1%}"'7.5%'

Найкорисніша комбінація — :>10.2f: фіксована ширина плюс фіксована точність. Саме вона перетворює купу чисел на охайну таблицю, де коми стоять одна під одною. Спробуй свою специфікацію:

Інтерактив 5 · Міні-мова формату

Одна специфікація — п'ять різних значень. Сірою смугою показано поле заданої ширини.

напр. >10.2f · ^12 · ,.2f · +.1f

вирівнювання
ширина
точність
тип
Що варто помітити: ширина — це мінімум, а не максимум. Якщо значення довше за поле, воно нікуди не обрізається — f"{1234567.891:>6.2f}" спокійно вилізе за межі. І другий момент: .2f на рядку дає ValueError, бо «два знаки після коми» для тексту не мають сенсу. Спробуй — інтерактив покаже точний текст помилки.
Де f-рядки не місце. Ніколи не збирай f-рядком SQL-запит, HTML-сторінку чи команду для оболонки з даними, які ввела людина. Це прямий шлях до ін'єкції: підставлений текст стає частиною команди. Для кожного з цих випадків є свій безпечний інструмент — параметризовані запити, шаблонізатори, списки аргументів.

08 / БайтиЛітери, коди й байти

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

крок 1 · символ і його номер

Unicode — це велика таблиця, у якій кожному символу світу присвоєно номер (кодову позицію). Латинська a має номер 97, українська ї — 1111, а смайлик 🙂 — 128578. Функція ord() показує номер символу, chr() робить зворотне.

крок 2 · номер треба якось записати

Номер — це абстрактне число. Щоб покласти його у файл чи послати мережею, його перетворюють на послідовність байтів. Правило перетворення зветься кодуванням, і сучасний стандарт де-факто — UTF-8. Він змінної довжини: латиниця займає 1 байт, кирилиця й більшість європейських абеток — 2, ієрогліфи — 3, емодзі — 4.

крок 3 · звідки береться 24

У записі Київ;кава;249.50 вісім кириличних літер по 2 байти — це 16, плюс вісім символів ASCII (дві крапки з комою, цифри й точка) по 1 байту. Разом 24.

Найважливіше: len() рахує символи, а не байти. len("їжак") — це 4, хоча в UTF-8 воно займає 8 байтів. Так і має бути: коли ти питаєш довжину слова, тебе цікавлять літери.

Інтерактив 6 · Символ → код → байти

Бірюзові біти — службовий каркас UTF-8, рожеві — власне номер символу.

один символ

ord
Unicode
байтів UTF-8
hex
Як читати каркас: у одного байта старший біт нульовий — це чиста ASCII. Для двох байтів перший починається на 110, для трьох — на 1110, для чотирьох — на 11110; кількість одиниць = кількість байтів. Кожен наступний байт починається на 10. Саме тому UTF-8 самосинхронізується: за будь-яким байтом видно, він початковий чи продовження.

str і bytes — це різні типи

.encode() перетворює рядок на байти, .decode() — назад:

"Київ".encode() → b'\xd0\x9a\xd0\xb8\xd1\x97\xd0\xb2' (8 байтів)
b'\xd0\x9a\xd0\xb8\xd1\x97\xd0\xb2'.decode() → 'Київ' (4 символи)

Тип bytes пишеться з префіксом b і складається не з літер, а з чисел від 0 до 255. Складати його з рядком не можна: "Київ" + b"..." дасть TypeError: can only concatenate str (not "bytes") to str. Це навмисна стіна. У Python 2 такої стіни не було — і саме звідти походить ціле покоління програмістів з нервовим тиком на слові «кодування».

Звідки беруться крякозябри. Якщо байти, записані в UTF-8, прочитати як cp1251, помилки не буде — просто вийде сміття: "Київ".encode().decode("cp1251") дає 'РљРёС—РІ'. Ось чому кодування треба знати, а не вгадувати: неправильне часто спрацьовує «успішно». А коли байти взагалі не складаються у валідний UTF-8, буде чесний UnicodeDecodeError.

09 / ЦінаЧому += коштує дорого

Тепер повернімось до незмінності — і подивимось, у що вона обходиться, коли рядок треба складати шматками. Циклів ми ще не вчили (це тема 12), але ідея не потребує синтаксису: уяви, що ту саму дію повторюють багато разів поспіль.

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

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

Інтерактив 7 · += проти join: скільки роботи насправді

Кожен шматок — 6 символів. Рахуємо не час (він залежить від машини), а скопійовані символи й створені обʼєкти.

обʼєктів (+=)
обʼєктів (join)
символів (+=)
символів (join)
Що на графіку: рожева крива — сумарно скопійовані символи для +=, бірюзова — для join. Перша — парабола, друга — пряма. На дванадцяти шматках різниця смішна, на шістдесяти — вже тридцятикратна, а на десяти тисячах рядків із файла це різниця між «миттєво» і «пішов пити каву».
Чесне уточнення для профі. CPython має оптимізацію: якщо на рядок є рівно одне посилання, s += t іноді дописує на місці через realloc, і квадратичність зникає. Але покладатись на неї не можна — вона ламається від будь-якого другого посилання, її немає в PyPy, і вона не працює, коли рядок лежить у списку чи атрибуті. join швидкий завжди й у всіх реалізаціях. Правило просте: збираєш багато — збирай список шматків і склей один раз.

10 / НюансиДрібниці для тих, хто вже пише

Однакові рядки — не завжди один об'єкт

Пам'ятаєш is проти == з теми 03? Рядки — найкраще місце, щоб на цьому обпектися. Python економить пам'ять і інтернує (interning) частину рядків: тримає їх у спільній таблиці, щоб однаковий текст існував в одному екземплярі. Автоматично туди потрапляють ті, що схожі на ідентифікатори — самі лише латинські літери, цифри й підкреслення:

a = "kava"
b = "kava"
a is b # True — рядок схожий на ідентифікатор, його інтерновано

c = "кава"
d = "кава"
c is d # як пощастить: залежить від того, як компілювався код

e = "".join(["ка", "ва"])
e == c # True — зміст однаковий
e is c # False — обʼєкт зібрано під час роботи

Третій випадок — залізний: рядок, склеєний під час роботи програми, завжди окремий об'єкт. Другий — ні: у файлі-модулі компілятор зазвичай зводить два однакові літерали в одну константу, а в Jupyter той самий код дає False. Саме тому висновок той самий, що й для чисел: рядки порівнюють через ==. is для них — джерело помилок, які проявляються раз на сто запусків. Якщо однаковість об'єктів справді потрібна (наприклад, мільйон повторюваних ключів у пам'яті), є явний інструмент sys.intern(), який кладе рядок у спільну таблицю сам.

Усередині Python зберігає не UTF-8

Здавалося б, після розділу 08 логічно припустити, що рядок у пам'яті лежить у UTF-8. Ні. CPython дивиться на найбільшу кодову позицію в рядку й обирає однакову ширину для всіх символів: 1 байт, якщо всі вміщаються, 2 байти, якщо є кирилиця, 4 — якщо є емодзі. Тому sys.getsizeof("abcd") дає 45 байтів, а sys.getsizeof("їжак") — уже 66.

Плата за пам'ять, зате виграш принциповий: індексація й довжина працюють за сталий час. Щоб дістати рядок[1000], достатньо арифметики, а не проходу з початку. У мовах, що тримають текст у UTF-8 напряму, «взяти тисячний символ» коштує тисячу кроків. Ось чому str і bytes у Python — принципово різні типи, а не два погляди на ті самі дані.

Порівняння рядків — за кодовими позиціями

"а" < "б" дає True, і здається, що сортування працює за абеткою. Не працює. Порівняння йде посимвольно за номерами Unicode, а вони збігаються з українською абеткою лише випадково. Літера ґ має номер 1169 — після всіх решти кириличних:

sorted(["ґанок", "яблуко", "їжак", "абрикос"])
['абрикос', 'яблуко', 'їжак', 'ґанок']

Для справжнього алфавітного порядку потрібне мовне правило сортування (collation) — або своя таблиця ваг символів, або зовнішня бібліотека. Просто пам'ятай: типове сортування рядків — це порядок кодів, а не абетка.

11 / ПідсумокЩо з цього забрати

І наскрізна думка курсу, яка тут прозвучала вдруге: більшість «дивацтв» Python — це не дивацтва, а наслідки одного простого рішення. Незмінність рядка пояснює одразу все — і TypeError при спробі присвоєння, і те, що методи нічого не міняють, і квадратичність +=, і чому рядок можна класти ключем у словник. Одна властивість, шість наслідків.

Що далі. Ми весь час говорили, що рядок — це послідовність. Наступна тема — списки: теж послідовність, з тими самими індексами й зрізами, але змінювана. Там список[0] = "Л" спрацює — і одразу вилізуть усі ті ефекти спільного володіння, про які ми говорили в темі 03. А в practice.ipynb ти зараз розбереш наш запис на поля, складеш із них вирівняний звіт і власноруч переконаєшся, що байтів справді 24.

Далі в темі

Теорію прочитано. Тепер закріпи її на практиці.