У темі 03 ми домовились, що ім'я — це ярлик, наліплений на об'єкт, і що об'єкти бувають незмінні та змінювані. У темі 04 виявилось, що числа належать до першої групи: не можна «зробити з п'ятірки шістку», можна лише перевісити ярлик на іншу п'ятірку чи шістку. Тепер черга тексту. І тут та сама історія повторюється з несподіваною силою: рядок теж не змінюється ніколи, а кожна дія над ним створює новий об'єкт.
Наскрізний приклад у нас буде один — рядок із даними, який міг би прилетіти з файла чи з мережі:
Шістнадцять символів. З них ми навчимося витягати місто, назву товару й ціну, порахуємо, скільки це в байтах (спойлер: не шістнадцять), складемо з них охайний звіт — і побачимо щонайменше три способи зробити це неправильно.
01 / ЛапкиЯк народжується рядок
Рядок (string) — це літерал у лапках. Лапки бувають одинарні й подвійні, і різниці
між ними немає жодної: 'кава' і "кава" дають однаковий
об'єкт. Два варіанти існують рівно з однієї причини — щоб покласти всередину лапки іншого
виду без зайвої мороки:
Якщо всередині потрібні саме такі лапки, якими рядок відкрито, їх екранують
(escape) зворотним слешем: 'він сказав \'ні\''. Зворотний слеш вмикає спеціальне
значення наступного символу — і саме тому сам слеш теж доводиться подвоювати.
| запис | що означає | де трапляється |
|---|---|---|
| \n | перехід на новий рядок | текстові файли, друк у кілька рядків |
| \t | табуляція | вирівнювання колонок у консолі |
| \\ | сам зворотний слеш | шляхи Windows, регулярні вирази |
| \' \" | лапка всередині таких самих лапок | цитати |
| \u0457 | символ за його кодом Unicode | коли символу немає на клавіатурі |
Третій спосіб — потрійні лапки """…""". Усередині них можна
вільно тиснути Enter: усі переходи на новий рядок збережуться в самому рядку. Це основний
спосіб записати багаторядковий текст — і водночас те, чим пишуть документацію до функцій
(побачимо в темі 14).
І четвертий, який рятує від війни зі слешами, — сирий рядок (raw string):
літера r перед лапками вимикає обробку екранувань. r"C:\новий\текст"
залишиться саме таким, як написано, а без r Python побачив би там \n
і \t і мовчки зіпсував шлях. Регулярні вирази (тема 23) пишуть виключно так.
"Ки" "їв" — це "Київ", без жодного плюса.
Зручно для довгих текстів, розбитих на кілька рядків коду. Але якщо десь у переліку рядків
забути кому, замість двох елементів мовчки з'явиться один склеєний — і помилка не покажеться
доти, доки хтось не перерахує елементи.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 символів. Головна дивина тут — що stop не входить
у результат. І це не примха, а рішення, яке дає дві дуже зручні властивості:
- довжина зрізу = stop − start.
запис[0:4]— це рівно чотири символи, рахувати нічого не треба; - шматки стикуються без дірок і без нахлесту.
запис[:5] + запис[5:]дорівнює вихідному рядку — для будь-якого місця розрізу.
Будь-яке з трьох чисел можна пропустити, і тоді Python підставить розумне значення:
start — початок, stop — кінець, step — одиницю. Тому
запис[:4] — це «перші чотири», запис[-6:] — «останні шість», а
запис[:] — увесь рядок цілком.
Окремо варто запам'ятати ідіому рядок[::-1]: від'ємний крок розвертає обхід,
і ти отримуєш рядок навпаки. Покрути повзунки й подивись, як усе це виглядає на нашому записі.
Інтерактив 1 · Конструктор зрізу
Три повзунки — три числа зрізу. Під рядком два ряди чисел: додатні індекси й від'ємні. Стрілки показують порядок обходу.
stop = 18 при довжині 16, і
Python просто віддасть те, що є, без IndexError. Це принципова відмінність
зрізу від індексу — і тому зрізом безпечніше брати «перші п'ять символів» із рядка,
довжини якого ти не знаєш.запис[:] для рядка
повертає той самий об'єкт, а не копію: запис[:] is запис дає
True. Копіювати незмінний об'єкт немає сенсу, тому Python цього й не робить.
Для списків (тема 06) той самий запис уже створить справжню копію — саме тому, що список
змінюваний.04 / НезмінністьРядок не змінюється ніколи
А тепер головне речення теми. Спробуй виправити першу літеру:
Python відповість TypeError: 'str' object does not support item assignment.
Не «помилка доступу», не «немає прав» — тип не підтримує таку операцію.
Рядок незмінний (immutable), і жодного способу відредагувати його на місці не існує.
Звідси головний наслідок, який визначає весь стиль роботи з текстом:
кожна «зміна» рядка — це насправді створення нового рядка. Методи
.upper(), .replace(), .strip() нічого не правлять —
вони повертають новий об'єкт, а старий лишається як був. Найчастіша помилка новачка
виглядає рівно так:
напій.upper() # новий рядок створено... і викинуто
print(напій) # кава — бо ми нічого нікуди не записали
Правильно — перевісити ярлик: напій = напій.upper(). Порівняй усі три випадки
в інтерактиві, дивлячись на id об'єктів до і після.
Інтерактив 2 · Що насправді відбувається при «зміні»
Три спроби змінити рядок. Стеж за ярликом і за id об'єктів.
напій перевішено
на нього; старий лишився без жодного ярлика й буде прибраний. У третьому новий об'єкт теж
створено — але ярлик на нього не поставили, тож він зникає в ту саму мить.Навіщо взагалі така незручність
Незмінність — не покарання, а плата за три конкретні речі, кожна з яких коштує дорого:
- Рядок можна класти в словник ключем. Хеш незмінного об'єкта рахується один раз і більше ніколи не змінюється. Якби рядок можна було редагувати, ключ у словнику «поїхав» би й значення загубилося. Про це — тема 08;
- Рядок безпечно передавати куди завгодно. Ти точно знаєш, що функція, якій ти дав ім'я користувача, не перепише його всередині;
- Однакові рядки можна не дублювати в пам'яті. Про це — в розділі 10.
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.
будь-який підрядок
-1: це не «нічого», це цілком валідний
індекс — останній символ. Тому перевірка if запис.find(x): хибна двічі: вона
пропустить знахідку на позиції 0 і прийме −1 за успіх. Пиши
if запис.find(x) >= 0:, а ще краще — if x in запис:.06 / РозбірРозібрати й зібрати
Дві операції, які зустрічаються в реальному коді частіше за всі інші разом узяті.
.split(роздільник) ріже рядок на частини. .join(частини) склеює
їх назад. Разом вони утворюють точну пару: розібрав, полагодив, зібрав.
Три тонкощі, на яких спотикаються всі.
.split()без аргументу — це інша операція. Вона ріже по будь-якій кількості будь-яких пробільних символів і викидає порожні шматки з країв. Тому"a b ".split()дає два елементи, а"a b ".split(" ")— аж чотири, два з яких порожні;- роздільник не «зникає» частково. Якщо він трапився двічі поспіль, між ними народиться порожній рядок. Це не баг: інакше не можна було б відрізнити «поле порожнє» від «поля немає»;
join— метод роздільника, а не списку. Пишеться навпаки до інтуїції:";".join(частини). Логіка тут така: клеїти можна що завгодно, а от чим клеїти — це і є рядок, тому метод його.
Інтерактив 4 · split і join у обидва боки
Той самий запис, але «брудний»: із зайвими пробілами по краях і навколо роздільників.
клей для join
";" без .strip() —
і подивись на пробіли, що прилипли до кожної частини. Саме через них потім
"кава" == " кава " дає False, і людина півгодини шукає помилку
в даних. Далі перемкни на «без арг.» — побачиш, що .split() вважає окремим
словом навіть самотню крапку з комою.";".join(["кава", 3])
впаде з TypeError: sequence item 1: expected str instance, int found. Числа
доведеться перетворити явно — і це навмисна суворість: Python не вгадує, як саме ти хотів
надрукувати число.07 / Форматf-рядки й міні-мова формату
Складати текст із плюсів — виснажливо й нечитабельно. Сучасний спосіб один:
f-рядок (formatted string literal). Літера f перед лапками
вмикає підстановку виразів у фігурних дужках:
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 | доповнити нулями до ширини 8 | f"{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, бо «два знаки після коми» для тексту не мають сенсу. Спробуй —
інтерактив покаже точний текст помилки.08 / БайтиЛітери, коди й байти
Тепер обіцяний спойлер: у нашому записі шістнадцять символів, але двадцять чотири байти. Щоб зрозуміти чому, треба розділити три поняття, які в побуті злипаються в одне.
Unicode — це велика таблиця, у якій кожному символу світу присвоєно номер
(кодову позицію). Латинська a має номер 97, українська
ї — 1111, а смайлик 🙂 — 128578. Функція ord() показує номер
символу, chr() робить зворотне.
Номер — це абстрактне число. Щоб покласти його у файл чи послати мережею, його перетворюють на послідовність байтів. Правило перетворення зветься кодуванням, і сучасний стандарт де-факто — UTF-8. Він змінної довжини: латиниця займає 1 байт, кирилиця й більшість європейських абеток — 2, ієрогліфи — 3, емодзі — 4.
У записі Київ;кава;249.50 вісім кириличних літер по 2 байти — це 16, плюс
вісім символів ASCII (дві крапки з комою, цифри й точка) по 1 байту. Разом 24.
Найважливіше: len() рахує символи, а не байти.
len("їжак") — це 4, хоча в UTF-8 воно займає 8 байтів. Так і має бути: коли ти
питаєш довжину слова, тебе цікавлять літери.
Інтерактив 6 · Символ → код → байти
Бірюзові біти — службовий каркас UTF-8, рожеві — власне номер символу.
один символ
110, для трьох — на 1110,
для чотирьох — на 11110; кількість одиниць = кількість байтів. Кожен наступний
байт починається на 10. Саме тому UTF-8 самосинхронізується: за будь-яким
байтом видно, він початковий чи продовження.str і bytes — це різні типи
.encode() перетворює рядок на байти, .decode() — назад:
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 такої стіни не було — і саме звідти походить ціле покоління програмістів
з нервовим тиком на слові «кодування».
"Київ".encode().decode("cp1251") дає 'РљРёС—РІ'. Ось чому
кодування треба знати, а не вгадувати: неправильне часто спрацьовує «успішно».
А коли байти взагалі не складаються у валідний UTF-8, буде чесний
UnicodeDecodeError.09 / ЦінаЧому += коштує дорого
Тепер повернімось до незмінності — і подивимось, у що вона обходиться, коли рядок треба складати шматками. Циклів ми ще не вчили (це тема 12), але ідея не потребує синтаксису: уяви, що ту саму дію повторюють багато разів поспіль.
Запис результат += шматок виглядає як «дописати». Насправді він означає
результат = результат + шматок, а плюс над незмінними рядками — це
створити третій рядок і скопіювати в нього обидва. На другому кроці копіюється
вже вдвічі більше, на третьому — втричі. Кількість скопійованих символів росте
квадратично від кількості кроків.
"".join(частини) робить те саме за один прохід: спершу рахує сумарну довжину,
один раз виділяє пам'ять, один раз копіює кожен шматок. Лінійно замість квадратичного.
Інтерактив 7 · += проти join: скільки роботи насправді
Кожен шматок — 6 символів. Рахуємо не час (він залежить від машини), а скопійовані символи й створені обʼєкти.
+=, бірюзова — для join. Перша — парабола, друга — пряма.
На дванадцяти шматках різниця смішна, на шістдесяти — вже тридцятикратна, а на десяти
тисячах рядків із файла це різниця між «миттєво» і «пішов пити каву».s += t іноді дописує на місці через realloc,
і квадратичність зникає. Але покладатись на неї не можна — вона ламається від будь-якого
другого посилання, її немає в PyPy, і вона не працює, коли рядок лежить у списку чи атрибуті.
join швидкий завжди й у всіх реалізаціях. Правило просте: збираєш багато —
збирай список шматків і склей один раз.10 / НюансиДрібниці для тих, хто вже пише
Однакові рядки — не завжди один об'єкт
Пам'ятаєш is проти == з теми 03? Рядки — найкраще місце, щоб на
цьому обпектися. Python економить пам'ять і інтернує (interning) частину
рядків: тримає їх у спільній таблиці, щоб однаковий текст існував в одному екземплярі.
Автоматично туди потрапляють ті, що схожі на ідентифікатори — самі лише латинські літери,
цифри й підкреслення:
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 — після
всіх решти кириличних:
['абрикос', 'яблуко', 'їжак', 'ґанок']
Для справжнього алфавітного порядку потрібне мовне правило сортування (collation) — або своя таблиця ваг символів, або зовнішня бібліотека. Просто пам'ятай: типове сортування рядків — це порядок кодів, а не абетка.
11 / ПідсумокЩо з цього забрати
- Рядок незмінний.
s[0] = "X"не працює ніколи. Кожен метод повертає новий об'єкт — і якщо результат нікуди не записати, він зникне; - Зріз — головний інструмент.
s[start:stop:step], верхня межа не входить, будь-яке число можна пропустити, за межі не падає,s[::-1]розвертає; splitіjoin— пара. Розібрав, полагодив, зібрав.join— метод роздільника й приймає тільки рядки;- f-рядки — єдиний спосіб форматування, який варто вчити. Двокрапка
вмикає міні-мову: ширина, вирівнювання,
.2f,,; - Символи — не байти.
lenрахує символи,encodeдає байти, кирилиця коштує 2 байти на літеру, іstrзbytesне складаються; - Збираєш багато — не
+=, аjoin.
І наскрізна думка курсу, яка тут прозвучала вдруге: більшість «дивацтв» Python — це не
дивацтва, а наслідки одного простого рішення. Незмінність рядка пояснює одразу все — і
TypeError при спробі присвоєння, і те, що методи нічого не міняють, і
квадратичність +=, і чому рядок можна класти ключем у словник. Одна властивість,
шість наслідків.
список[0] = "Л" спрацює — і одразу вилізуть
усі ті ефекти спільного володіння, про які ми говорили в темі 03. А в
practice.ipynb ти зараз розбереш наш запис на поля, складеш із них вирівняний
звіт і власноруч переконаєшся, що байтів справді 24.Далі в темі
Теорію прочитано. Тепер закріпи її на практиці.