Практика · Рядки¶
Лекція: lecture.html · Тест: quiz.html · ДЗ: homework.md
Наскрізний приклад той самий, що й у лекції, — рядок із даними, який міг би прилетіти з файла:
запис = "Київ;кава;249.50"
Що ми зробимо власними руками:
- подивимось, як народжується рядок і навіщо йому три види лапок;
- розріжемо його зрізами й переконаємось, що межі стикуються без дірок;
- спробуємо змінити символ — і отримаємо
TypeError; - розберемо запис на поля через
splitі зберемо назад черезjoin; - напишемо свій
splitі зіставимо його з вбудованим; - складемо вирівняний звіт f-рядками й перевіримо своє вирівнювання проти
format; - порахуємо байти UTF-8 вручну й зіставимо з
len(рядок.encode()); - побачимо, скільки зайвої роботи робить
+=протиjoin.
Кожна клітинка щось друкує. Запускай згори вниз.
1 · Три види лапок і екранування¶
Одинарні й подвійні лапки дають однаковий об'єкт. Різниця лише в тому, що
зручніше покласти всередину. Потрійні лапки зберігають переходи на новий рядок,
а префікс r вимикає обробку зворотного слеша.
запис = "Київ;кава;249.50"
print("однакові лапки:", 'кава' == "кава")
print()
print('усередині подвійних:', "він сказав 'ні'")
print("усередині одинарних:", 'тип "б", ціна 249.50')
print("екранування: ", 'він сказав \'ні\'')
print()
# потрійні лапки зберігають переноси рядка так, як ти їх набрав
чек = """Місто: Київ
Товар: кава
Ціна: 249.50"""
print("багаторядковий текст:")
print(чек)
print()
print("у ньому переходів на новий рядок:", чек.count("\n"))
однакові лапки: True усередині подвійних: він сказав 'ні' усередині одинарних: тип "б", ціна 249.50 екранування: він сказав 'ні' багаторядковий текст: Місто: Київ Товар: кава Ціна: 249.50 у ньому переходів на новий рядок: 2
Сирий рядок: коли зворотний слеш має лишитися слешем¶
\n усередині звичайного рядка — це один символ переходу на новий рядок,
а не два символи «слеш» і «н». Префікс r цю обробку вимикає.
звичайний = "C:\new\table" # \n і \t тут — СПРАВЖНІ екранування
сирий = r"C:\new\table" # а тут просто символи
print("звичайний:", repr(звичайний), " довжина", len(звичайний))
print("сирий: ", repr(сирий), " довжина", len(сирий))
print("однакові? ", звичайний == сирий)
print()
print("ось як звичайний друкується — від шляху нічого не лишилось:")
print(звичайний)
print()
print("а сирий — саме те, що написано:")
print(сирий)
звичайний: 'C:\new\table' довжина 10 сирий: 'C:\\new\\table' довжина 12 однакові? False ось як звичайний друкується — від шляху нічого не лишилось: C: ew able а сирий — саме те, що написано: C:\new\table
Два літерали поруч склеюються самі¶
Це зручно для довгих текстів — і небезпечно там, де забули кому.
місто = "Ки" "їв" # без жодного плюса
print("«Ки» «їв» дає:", місто)
assert місто == "Київ", "склейка сусідніх літералів не спрацювала"
print("✅ сусідні літерали справді склеїлись в один рядок")
«Ки» «їв» дає: Київ ✅ сусідні літерали справді склеїлись в один рядок
2 · Довжина та індекси¶
Нумерація починається з нуля, бо індекс — це відстань від початку.
Від'ємний індекс -k означає те саме, що len(рядок) - k.
print("сам рядок:", запис)
print("довжина: ", len(запис))
print()
print("запис[0] =", запис[0], " (перший символ)")
print("запис[4] =", запис[4], " (перша крапка з комою)")
print("запис[15] =", запис[15], " (останній символ)")
print("запис[-1] =", запис[-1], " (він самий, але з кінця)")
print("запис[-6] =", запис[-6], " (початок ціни)")
print()
print("правило перекладу: запис[-6] == запис[len(запис) - 6] ->",
запис[-6] == запис[len(запис) - 6])
сам рядок: Київ;кава;249.50 довжина: 16 запис[0] = К (перший символ) запис[4] = ; (перша крапка з комою) запис[15] = 0 (останній символ) запис[-1] = 0 (він самий, але з кінця) запис[-6] = 2 (початок ціни) правило перекладу: запис[-6] == запис[len(запис) - 6] -> True
За межі вийти не можна¶
IndexError — це добра помилка: вона зупиняє програму одразу, а не дає їй
далі працювати зі сміттям.
try:
print(запис[16])
except IndexError as помилка:
print("запис[16] -> IndexError:", помилка)
print()
print("а от зріз за межами не падає взагалі:")
print(" запис[10:100] =", repr(запис[10:100]))
print(" запис[50:60] =", repr(запис[50:60]), " (просто порожньо)")
запис[16] -> IndexError: string index out of range а от зріз за межами не падає взагалі: запис[10:100] = '249.50' запис[50:60] = '' (просто порожньо)
3 · Зрізи: [start:stop:step]¶
Верхня межа не входить у результат. Саме тому довжина зрізу дорівнює
stop - start, а сусідні шматки стикуються без дірок і без нахлесту.
print("запис =", запис)
print("запис[:4] =", запис[:4], " місто")
print("запис[5:9] =", запис[5:9], " товар")
print("запис[-6:] =", запис[-6:], " ціна")
print("запис[::2] =", запис[::2], " кожен другий символ")
print("запис[::-1] =", запис[::-1], " увесь рядок навпаки")
print()
print("довжина зрізу [5:9] =", len(запис[5:9]), " а stop - start =", 9 - 5)
запис = Київ;кава;249.50 запис[:4] = Київ місто запис[5:9] = кава товар запис[-6:] = 249.50 ціна запис[::2] = Кї;аа295 кожен другий символ запис[::-1] = 05.942;авак;вїиК увесь рядок навпаки довжина зрізу [5:9] = 4 а stop - start = 4
Головна властивість напіввідкритого інтервалу¶
рядок[:k] + рядок[k:] дає вихідний рядок — для будь-якого місця розрізу.
Перевіримо це на всіх можливих k одразу.
# цикл ще попереду за програмою (тема 12) — тут він лише щоб перебрати всі місця розрізу
розбіжностей = 0
for місце in range(len(запис) + 1):
if запис[:місце] + запис[місце:] != запис:
розбіжностей += 1
print("перевірено місць розрізу:", len(запис) + 1)
print("розбіжностей:", розбіжностей)
assert розбіжностей == 0, "зрізи не стикуються — щось не так з розумінням меж"
print()
print("✅ рядок[:k] + рядок[k:] == рядок для всіх k від 0 до", len(запис))
перевірено місць розрізу: 17 розбіжностей: 0 ✅ рядок[:k] + рядок[k:] == рядок для всіх k від 0 до 16
Зріз усього рядка не створює копії¶
Для незмінного об'єкта копія не має сенсу — тому рядок[:] повертає
той самий об'єкт. Порівняй із темою 03: is питає про тотожність об'єктів,
а не про рівність значень.
копія = запис[:]
print("копія == запис ->", копія == запис, " (значення однакове)")
print("копія is запис ->", копія is запис, " (і обʼєкт теж той самий!)")
print()
print("id(запис) =", id(запис))
print("id(копія) =", id(копія))
assert копія is запис, "для рядка зріз [:] мав би повернути той самий обʼєкт"
print()
print("✅ Python не копіює те, що однаково неможливо змінити")
копія == запис -> True (значення однакове) копія is запис -> True (і обʼєкт теж той самий!) id(запис) = 139974252424960 id(копія) = 139974252424960 ✅ Python не копіює те, що однаково неможливо змінити
4 · Незмінність: головна властивість теми¶
Спроба присвоїти символ зупиняє програму. Не «не спрацювала», а тип не має такої операції взагалі.
try:
запис[0] = "Л"
except TypeError as помилка:
print('запис[0] = "Л" -> TypeError:', помилка)
print()
print("а ось як робиться «зміна першого символу» насправді:")
новий = "Л" + запис[1:]
print(" було:", запис)
print(" стало:", новий)
print()
print("id старого:", id(запис))
print("id нового: ", id(новий), " — це інший обʼєкт")
запис[0] = "Л" -> TypeError: 'str' object does not support item assignment а ось як робиться «зміна першого символу» насправді: було: Київ;кава;249.50 стало: Лиїв;кава;249.50 id старого: 139974252424960 id нового: 139974252528912 — це інший обʼєкт
Найпоширеніша помилка новачка¶
Методи повертають новий рядок і нічого не міняють на місці. Якщо результат нікуди не записати, він зникне тієї ж миті.
напій = "кава"
id_до = id(напій)
напій.upper() # результат створено... і викинуто
print("після напій.upper() без присвоєння:", напій)
print("id не змінився:", id(напій) == id_до)
print()
напій = напій.upper() # ось тепер ярлик перевішено
print("після напій = напій.upper(): ", напій)
print("id змінився:", id(напій) != id_до)
assert напій == "КАВА"
print()
print("✅ метод повертає новий рядок — його треба кудись записати")
після напій.upper() без присвоєння: кава id не змінився: True після напій = напій.upper(): КАВА id змінився: True ✅ метод повертає новий рядок — його треба кудись записати
5 · Методи: регістр і краї¶
Регістр — властивість мови, а не байтів. Німецька ß у верхньому регістрі
перетворюється на дві літери, тому довжина рядка може змінитись.
товар = "кава мелена"
print("upper(): ", товар.upper())
print("title(): ", товар.title())
print("capitalize():", товар.capitalize())
print("lower(): ", "КАВА МЕЛЕНА".lower())
print()
print('"ß".upper() =', repr("ß".upper()), " довжина була 1, стала", len("ß".upper()))
print()
# casefold агресивніший за lower і призначений саме для порівняння
print('"Straße".casefold() =', "Straße".casefold())
print('"STRASSE".casefold() =', "STRASSE".casefold())
print("порівняння через casefold:", "Straße".casefold() == "STRASSE".casefold())
print("а через lower: ", "Straße".lower() == "STRASSE".lower())
upper(): КАВА МЕЛЕНА title(): Кава Мелена capitalize(): Кава мелена lower(): кава мелена "ß".upper() = 'SS' довжина була 1, стала 2 "Straße".casefold() = strasse "STRASSE".casefold() = strasse порівняння через casefold: True а через lower: False
Пастка strip з аргументом¶
.strip("абв") знімає будь-які символи з набору, а не підрядок «абв».
Якщо потрібен саме префікс чи суфікс — є точні методи.
брудний = " Київ ; кава ; 249.50 "
print("як є: ", repr(брудний))
print("strip(): ", repr(брудний.strip()))
print("lstrip(): ", repr(брудний.lstrip()))
print("rstrip(): ", repr(брудний.rstrip()))
print()
print('очікувано «кава», а насправді:', repr("кава".strip("ак")))
print(" бо злетіли «к» і «а» зліва та «а» справа — це набір символів, не підрядок")
print()
print('removeprefix("Київ;"):', repr(запис.removeprefix("Київ;")))
print('removesuffix(".50"): ', repr(запис.removesuffix(".50")))
як є: ' Київ ; кава ; 249.50 '
strip(): 'Київ ; кава ; 249.50'
lstrip(): 'Київ ; кава ; 249.50 '
rstrip(): ' Київ ; кава ; 249.50'
очікувано «кава», а насправді: 'в'
бо злетіли «к» і «а» зліва та «а» справа — це набір символів, не підрядок
removeprefix("Київ;"): 'кава;249.50'
removesuffix(".50"): 'Київ;кава;249'
6 · Пошук: find, count, in¶
find повертає індекс першого входження або -1. Це найнебезпечніше число
теми: воно і не «хибне» для умови, і водночас є валідним індексом останнього
символу.
print('запис.find(";") =', запис.find(";"))
print('запис.rfind(";") =', запис.rfind(";"))
print('запис.count(";") =', запис.count(";"))
print('запис.find("кава") =', запис.find("кава"))
print('запис.find("гроші") =', запис.find("гроші"), " <- не знайдено")
print()
print('"кава" in запис =', "кава" in запис)
print('"гроші" in запис =', "гроші" in запис)
print('запис.startswith("Київ") =', запис.startswith("Київ"))
print('запис.endswith(".50") =', запис.endswith(".50"))
запис.find(";") = 4
запис.rfind(";") = 9
запис.count(";") = 2
запис.find("кава") = 5
запис.find("гроші") = -1 <- не знайдено
"кава" in запис = True
"гроші" in запис = False
запис.startswith("Київ") = True
запис.endswith(".50") = True
Чому if рядок.find(x): — хибна перевірка¶
Вона помиляється двічі: пропускає знахідку на позиції 0 (бо 0 хибний)
і приймає -1 за успіх (бо -1 істинний).
шукане = "Київ" # стоїть на самому початку, індекс 0
print("наївна перевірка if запис.find(шукане):")
if запис.find(шукане):
print(" знайшло")
else:
print(" «не знайшло» — а насправді воно на позиції", запис.find(шукане))
print()
print("правильна перевірка:")
if запис.find(шукане) >= 0:
print(" знайшло на позиції", запис.find(шукане))
print()
print("найпростіша й найчитабельніша:")
if шукане in запис:
print(" знайшло")
наївна перевірка if запис.find(шукане):
«не знайшло» — а насправді воно на позиції 0 правильна перевірка: знайшло на позиції 0 найпростіша й найчитабельніша: знайшло
7 · split і join: розібрати й зібрати¶
.split(роздільник) ріже рядок на частини, .join(частини) склеює їх назад.
Зверни увагу: join — метод роздільника, а не списку.
поля = запис.split(";")
print("запис.split(';') ->", поля)
print("частин:", len(поля))
print()
місто, товар, ціна_рядком = поля[0], поля[1], поля[2]
print("місто =", місто)
print("товар =", товар)
print("ціна =", ціна_рядком, " (це поки що рядок, не число!)")
print()
ціна = float(ціна_рядком)
print("після float():", ціна, type(ціна))
запис.split(';') ->
['Київ', 'кава', '249.50'] частин: 3 місто = Київ товар = кава ціна = 249.50 (це поки що рядок, не число!) після float(): 249.5 <class 'float'>
Три тонкощі, на яких спотикаються всі¶
брудний = " Київ ; кава ; 249.50 "
print("1) .split() без аргументу — окрема операція:")
print(" по пробілах:", брудний.split())
print(" по ';': ", брудний.split(";"))
print()
print("2) роздільник поспіль дає порожні частини:")
print(" 'a;;b'.split(';') =", "a;;b".split(";"))
print(" ';a;'.split(';') =", ";a;".split(";"))
print()
print("3) join приймає ЛИШЕ рядки:")
try:
";".join(["кава", 3])
except TypeError as помилка:
print(" ';'.join(['кава', 3]) -> TypeError:", помилка)
print(" правильно:", ";".join(["кава", str(3)]))
1) .split() без аргументу — окрема операція:
по пробілах: ['Київ', ';', 'кава', ';', '249.50']
по ';': [' Київ ', ' кава ', ' 249.50 ']
2) роздільник поспіль дає порожні частини:
'a;;b'.split(';') = ['a', '', 'b']
';a;'.split(';') = ['', 'a', '']
3) join приймає ЛИШЕ рядки:
';'.join(['кава', 3]) -> TypeError: sequence item 1: expected str instance, int found
правильно: кава;3
Прибираємо пробіли й збираємо назад¶
Класичний конвеєр: розрізали → почистили кожну частину → склеїли новим клеєм.
частини = брудний.split(";")
print("як розрізалось:", частини)
# чистимо кожну частину окремо; цикл — тема 12, тут він лише щоб не писати три рядки
чисті = []
for частина in частини:
чисті.append(частина.strip())
print("після strip(): ", чисті)
print()
print("склеюємо назад через ' | ':")
print(" ", " | ".join(чисті))
print("склеюємо назад через ';':")
print(" ", ";".join(чисті))
assert ";".join(чисті) == запис, "після чистки й склеювання мав вийти вихідний запис"
print()
print("✅ розібрали брудний рядок і зібрали з нього рівно наш запис")
як розрізалось: [' Київ ', ' кава ', ' 249.50 '] після strip(): ['Київ', 'кава', '249.50'] склеюємо назад через ' | ': Київ | кава | 249.50 склеюємо назад через ';': Київ;кава;249.50 ✅ розібрали брудний рядок і зібрали з нього рівно наш запис
8 · Перевірка: наш split проти вбудованого¶
Найцінніше, що дає практика, — побачити, що всередині бібліотеки немає магії.
Напишемо власне розбиття, спираючись лише на find і зрізи.
def мій_split(рядок, роздільник):
"""Ріже рядок на частини так само, як str.split(роздільник).
Ідея: щоразу шукаємо найближчий роздільник, відрізаємо все до нього,
а працювати продовжуємо з тим, що лишилось після нього.
"""
частини = []
залишок = рядок
# цикл потрібен, бо роздільників може бути скільки завгодно (тема 12)
while роздільник in залишок:
межа = залишок.find(роздільник)
частини.append(залишок[:межа]) # усе до роздільника
залишок = залишок[межа + len(роздільник):] # усе після нього
частини.append(залишок) # хвіст після останнього
return частини
проби = [запис, "a;;b", ";a;", "без роздільника", "", ";;;"]
print(f"{'вхід':<20} {'наш':<28} {'вбудований'}")
print("-" * 78)
for проба in проби:
print(f"{проба!r:<20} {мій_split(проба, ';')!s:<28} {проба.split(';')}")
assert мій_split(проба, ";") == проба.split(";"), f"розійшлись на {проба!r}"
print()
print("✅ наш split дає те саме, що й вбудований, на всіх шести пробах")
вхід наш вбудований ------------------------------------------------------------------------------ 'Київ;кава;249.50' ['Київ', 'кава', '249.50'] ['Київ', 'кава', '249.50'] 'a;;b' ['a', '', 'b'] ['a', '', 'b'] ';a;' ['', 'a', ''] ['', 'a', ''] 'без роздільника' ['без роздільника'] ['без роздільника'] '' [''] [''] ';;;' ['', '', '', ''] ['', '', '', ''] ✅ наш split дає те саме, що й вбудований, на всіх шести пробах
9 · f-рядки: підстановка виразів¶
Літера f перед лапками вмикає підстановку. Усередині фігурних дужок може
стояти будь-який вираз, а не тільки ім'я.
кількість = 3
сума = ціна * кількість
print(f"{місто}: {товар} по {ціна} грн")
print(f"три пачки коштують {ціна * кількість} грн")
print()
print("!r друкує технічне представлення:")
print(f" звичайно: {товар}")
print(f" з !r: {товар!r}")
print()
print("а знак = друкує одразу і вираз, і значення — найшвидший спосіб налагодження:")
print(f" {ціна=}")
print(f" {сума=}")
print(f" {len(запис)=}")
Київ: кава по 249.5 грн три пачки коштують 748.5 грн !r друкує технічне представлення: звичайно: кава з !r: 'кава' а знак = друкує одразу і вираз, і значення — найшвидший спосіб налагодження: ціна=249.5 сума=748.5 len(запис)=16
Міні-мова формату: ширина, вирівнювання, точність¶
Після двокрапки описують, як саме друкувати. Порядок частин фіксований:
[заповнювач][< > ^][знак][ширина][,][.точність][тип].
print(f'{"праворуч":>20}|')
print(f'{"ліворуч":<20}|')
print(f'{"по центру":^20}|')
print(f'{"крапками":.^20}|')
print()
print("249.5 з двома знаками: ", f"{ціна:.2f}")
print("із роздільником тисяч: ", f"{1234567.891:,.2f}")
print("зі знаком: ", f"{ціна:+.2f}")
print("нулями до ширини 10: ", f"{ціна:010.2f}")
print("у відсотках: ", f"{0.075:.1%}")
print()
print("а ось помилка — числовий формат до тексту:")
try:
print(f"{товар:.2f}")
except ValueError as помилка:
print(" ValueError:", помилка)
праворуч|
ліворуч |
по центру |
......крапками......|
249.5 з двома знаками: 249.50
із роздільником тисяч: 1,234,567.89
зі знаком: +249.50
нулями до ширини 10: 0000249.50
у відсотках: 7.5%
а ось помилка — числовий формат до тексту:
ValueError: Unknown format code 'f' for object of type 'str'
Складаємо звіт¶
Три позиції, ціни різної довжини — і все одно рівна колонка. Саме заради цього й потрібні ширина з точністю.
позиції = [("кава мелена", 3, 249.5), ("цукор", 12, 34.9), ("чай зелений", 1, 1250.0)]
print(f"{'товар':<16}{'к-сть':>7}{'ціна':>12}{'разом':>14}")
print("-" * 49)
# цикл — лише щоб не писати три однакові рядки вручну
загальна = 0.0
for назва, шт, ціна_шт in позиції:
разом = шт * ціна_шт
загальна += разом
print(f"{назва:<16}{шт:>7}{ціна_шт:>12,.2f}{разом:>14,.2f}")
print("-" * 49)
print(f"{'РАЗОМ':<16}{'':>7}{'':>12}{загальна:>14,.2f}")
товар к-сть ціна разом
------------------------------------------------- кава мелена 3 249.50 748.50 цукор 12 34.90 418.80 чай зелений 1 1,250.00 1,250.00 ------------------------------------------------- РАЗОМ 2,417.30
Перевірка: наше вирівнювання проти format¶
Вирівнювання праворуч — це просто «дописати спереду стільки пробілів,
скільки бракує». Переконаємось, що вбудований format робить рівно те саме.
def моє_вирівнювання_праворуч(текст, ширина):
"""Доповнює текст пробілами зліва до заданої ширини.
Ширина — це мінімум: якщо текст довший, нічого не обрізається.
"""
бракує = ширина - len(текст)
if бракує <= 0:
return текст
return " " * бракує + текст
проби = [("кава", 10), ("249.50", 10), ("довгий рядок", 5), ("", 4)]
print(f"{'вхід':<24}{'наше':<18}{'format'}")
print("-" * 60)
for текст, ширина in проби:
наше = моє_вирівнювання_праворуч(текст, ширина)
бібліотечне = format(текст, ">" + str(ширина))
print(f"{(текст, ширина)!s:<24}{наше!r:<18}{бібліотечне!r}")
assert наше == бібліотечне, f"розійшлись на {текст!r}"
print()
print("✅ наше вирівнювання збіглося з format на всіх пробах")
вхід наше format
------------------------------------------------------------
('кава', 10) ' кава' ' кава'
('249.50', 10) ' 249.50' ' 249.50'
('довгий рядок', 5) 'довгий рядок' 'довгий рядок'
('', 4) ' ' ' '
✅ наше вирівнювання збіглося з format на всіх пробах
10 · Символи, коди й байти¶
len рахує символи, а не байти. Кириличний символ у UTF-8 займає два байти,
латинський — один, емодзі — чотири.
print("запис =", запис)
print("len(запис) =", len(запис), " символів")
print("байтів у UTF-8 =", len(запис.encode()), " байтів")
print()
print("ord('a') =", ord("a"), " chr(97) =", chr(97))
print("ord('ї') =", ord("ї"), " chr(1111) =", chr(1111))
print("ord('🙂') =", ord("🙂"), " (для Python це ОДИН символ)")
print()
print("len('їжак') =", len("їжак"))
print("len('їжак'.encode()) =", len("їжак".encode()))
assert len(запис) == 16, "у записі має бути 16 символів"
assert len(запис.encode()) == 24, "у записі має бути 24 байти"
print()
print("✅ 16 символів і 24 байти — розбіжність саме через кирилицю")
запис = Київ;кава;249.50
len(запис) = 16 символів
байтів у UTF-8 = 24 байтів
ord('a') = 97 chr(97) = a
ord('ї') = 1111 chr(1111) = ї
ord('🙂') = 128578 (для Python це ОДИН символ)
len('їжак') = 4
len('їжак'.encode()) = 8
✅ 16 символів і 24 байти — розбіжність саме через кирилицю
str і bytes — різні типи¶
encode перетворює рядок на байти, decode — назад. Складати їх один з одним
не можна: це навмисна стіна.
байти = "Київ".encode()
print("'Київ'.encode() =", байти)
print("тип:", type(байти), " довжина:", len(байти))
print("назад:", байти.decode())
print()
try:
"Київ" + байти
except TypeError as помилка:
print("'Київ' + b'...' -> TypeError:", помилка)
print()
print("а ось звідки беруться крякозябри — байти UTF-8 прочитали як cp1251:")
print(" ", байти.decode("cp1251"))
print(" помилки не було! неправильне кодування часто «спрацьовує успішно»")
'Київ'.encode() = b'\xd0\x9a\xd0\xb8\xd1\x97\xd0\xb2' тип: <class 'bytes'> довжина: 8 назад: Київ 'Київ' + b'...' -> TypeError: can only concatenate str (not "bytes") to str а ось звідки беруться крякозябри — байти UTF-8 прочитали як cp1251: РљРёС—РІ помилки не було! неправильне кодування часто «спрацьовує успішно»
Перевірка: наш підрахунок байтів проти encode¶
UTF-8 влаштований просто: кодова позиція до 128 — один байт, до 2048 — два, до 65536 — три, далі — чотири. Реалізуємо це правило й порівняємо з бібліотекою.
def байтів_на_символ(символ):
"""Скільки байтів займе цей символ у UTF-8 — за таблицею меж стандарту."""
код = ord(символ)
if код < 0x80: # 128 — уся латиниця й цифри
return 1
if код < 0x800: # 2048 — кирилиця й більшість європейських абеток
return 2
if код < 0x10000: # 65536 — ієрогліфи, валютні знаки
return 3
return 4 # емодзі та решта
проби = ["a", "ї", "€", "🙂", "їжак", "Львів", запис]
print(f"{'рядок':<20}{'символів':>10}{'наші байти':>13}{'encode':>9}")
print("-" * 52)
for проба in проби:
# цикл — лише щоб пройтись символами рядка (тема 12)
наші_байти = 0
for символ in проба:
наші_байти += байтів_на_символ(символ)
бібліотечні = len(проба.encode())
print(f"{проба!r:<20}{len(проба):>10}{наші_байти:>13}{бібліотечні:>9}")
assert наші_байти == бібліотечні, f"розійшлись на {проба!r}"
print()
print("рядок з емодзі трохи зсунув колонку: f-рядок рахує ширину в символах,")
print("а термінал малює емодзі завширшки у два — це вже не про Python")
print()
print("✅ наш підрахунок збігся з len(рядок.encode()) на всіх пробах")
рядок символів наші байти encode ---------------------------------------------------- 'a' 1 1 1 'ї' 1 2 2 '€' 1 3 3 '🙂' 1 4 4 'їжак' 4 8 8 'Львів' 5 10 10 'Київ;кава;249.50' 16 24 24 рядок з емодзі трохи зсунув колонку: f-рядок рахує ширину в символах, а термінал малює емодзі завширшки у два — це вже не про Python ✅ наш підрахунок збігся з len(рядок.encode()) на всіх пробах
11 · += проти join: рахуємо роботу¶
Кожне += над рядком створює новий об'єкт і копіює туди все, що вже було.
Тому на n шматках довжиною L копіюється L·(1+2+…+n) символів — квадратично.
join копіює кожен символ рівно один раз.
шматок = "кава; "
L = len(шматок)
n = 60
# рахуємо копіювання явно, крок за кроком (цикл — тема 12)
скопійовано_плюсом = 0
результат = ""
for крок in range(n):
результат += шматок
скопійовано_плюсом += len(результат) # стільки символів має новий обʼєкт
скопійовано_джойном = L * n
формула = L * n * (n + 1) // 2
print("довжина шматка L =", L, " кількість шматків n =", n)
print()
print("скопійовано символів через += :", скопійовано_плюсом)
print("те саме за формулою L·n(n+1)/2 :", формула)
print("скопійовано символів через join:", скопійовано_джойном)
print("різниця:", round(скопійовано_плюсом / скопійовано_джойном, 1), "раза")
print()
print("створено проміжних обʼєктів: += ->", n, " join ->", 1)
assert скопійовано_плюсом == формула, "квадратична формула не збіглася з підрахунком"
print()
print("✅ підрахунок крок за кроком збігся з формулою L·n(n+1)/2")
довжина шматка L = 6 кількість шматків n = 60 скопійовано символів через += : 10980 те саме за формулою L·n(n+1)/2 : 10980 скопійовано символів через join: 360 різниця: 30.5 раза створено проміжних обʼєктів: += -> 60 join -> 1 ✅ підрахунок крок за кроком збігся з формулою L·n(n+1)/2
частини = [шматок] * n # той самий шматок n разів
через_join = "".join(частини)
print("результати збігаються:", через_join == результат)
print("довжина обох:", len(через_join))
assert через_join == результат, "join мав дати той самий рядок, що й цикл із +="
print()
print("✅ той самий результат — але роботи в",
round(скопійовано_плюсом / скопійовано_джойном, 1), "раза менше")
результати збігаються: True довжина обох: 360 ✅ той самий результат — але роботи в 30.5 раза менше
12 · Інтернування: коли однакові рядки — один об'єкт¶
Python тримає частину рядків у спільній таблиці, щоб однаковий текст існував в одному екземплярі. Автоматично туди потрапляють ті, що схожі на ідентифікатори: самі лише латинські літери, цифри й підкреслення. Для решти — як пощастить. А рядок, зібраний під час роботи програми, — завжди окремий об'єкт.
Зверни увагу на другий рядок виводу. У файлі .py два однакові кириличні
літерали зазвичай дають True, а тут, у зошиті, — False. Це і є найкращий
аргумент проти is: відповідь залежить від того, як компілювався код.
import sys
латиною_1 = "kava"
латиною_2 = "kava" # схоже на ідентифікатор — інтернується автоматично
кирилицею_1 = "кава"
кирилицею_2 = "кава" # не схоже — гарантій немає
зібраний = "".join(["ка", "ва"]) # той самий текст, але склеєний у рантаймі
print('"kava" is "kava" ->', латиною_1 is латиною_2, " (інтерновано)")
print('"кава" is "кава" ->', кирилицею_1 is кирилицею_2, " (як пощастить!)")
print("зібраний is літерал ->", зібраний is кирилицею_1, " (завжди інший обʼєкт)")
print()
print("а от значення в усіх трьох однакове:")
print(" зібраний == кирилицею_1 ->", зібраний == кирилицею_1)
print()
print("id(кирилицею_1) =", id(кирилицею_1))
print("id(зібраний) =", id(зібраний))
print()
спільний = sys.intern(зібраний) # явно кладемо в спільну таблицю
print("sys.intern(зібраний) is зібраний ->", спільний is зібраний)
assert зібраний == кирилицею_1 and зібраний is not кирилицею_1
print()
print("✅ рівність значень і тотожність обʼєктів — різні питання;")
print(" для рядків завжди питай ==, а не is")
"kava" is "kava" -> True (інтерновано) "кава" is "кава" -> False (як пощастить!) зібраний is літерал -> False (завжди інший обʼєкт) а от значення в усіх трьох однакове: зібраний == кирилицею_1 -> True id(кирилицею_1) = 139974252949424 id(зібраний) = 139974252948464 sys.intern(зібраний) is зібраний -> True ✅ рівність значень і тотожність обʼєктів — різні питання; для рядків завжди питай ==, а не is
13 · Сортування рядків — це порядок кодів, а не абетка¶
Рядки порівнюються посимвольно за кодовими позиціями Unicode. Українська
ґ має номер 1169 — після всіх інших кириличних літер, тому в кінці списку.
слова = ["ґанок", "яблуко", "їжак", "абрикос"]
print("як відсортував Python:", sorted(слова))
print()
print("а ось чому — подивись на номери перших літер:")
print(" 'а' ->", ord("а"))
print(" 'я' ->", ord("я"))
print(" 'ї' ->", ord("ї"))
print(" 'ґ' ->", ord("ґ"))
print()
print("'Я' < 'а' ->", "Я" < "а", " бо великі літери мають менші номери:",
ord("Я"), "<", ord("а"))
assert sorted(слова) == ["абрикос", "яблуко", "їжак", "ґанок"]
print()
print("✅ типове сортування рядків — порядок кодів, а не українська абетка")
як відсортував Python: ['абрикос', 'яблуко', 'їжак', 'ґанок'] а ось чому — подивись на номери перших літер: 'а' -> 1072 'я' -> 1103 'ї' -> 1111 'ґ' -> 1169 'Я' < 'а' -> True бо великі літери мають менші номери: 1071 < 1072 ✅ типове сортування рядків — порядок кодів, а не українська абетка
Підсумок¶
Що ми перевірили власними руками:
рядок[:k] + рядок[k:] == рядокдля всіх місць розрізу;рядок[:]для рядка повертає той самий об'єкт, а не копію;- присвоєння в символ дає
TypeError, а «зміна» — це новий об'єкт з новимid; - наш
splitнаfindі зрізах дає те саме, що вбудований; - наше вирівнювання пробілами дає те саме, що
format(текст, ">N"); - наш підрахунок байтів за межами UTF-8 дає те саме, що
len(рядок.encode()); - покрокове копіювання при
+=точно збігається з формулоюL·n(n+1)/2; - однакові за значенням рядки можуть бути різними об'єктами.
Головне: всередині рядкових методів немає магії. Там ті самі кілька рядків, які ти щойно написав сам — просто виконані швидким кодом на C.
Завдання¶
🟢 Рівень 1 — База¶
Візьми рядок " Львів ; чай зелений ; 1250.00 ; 2 " і розбери його на
чотири поля: місто, товар, ціну (числом) і кількість (цілим). Надрукуй їх
по одному в рядок.
Зроблено, якщо: виконується
assert поля == ["Львів", "чай зелений", "1250.00", "2"]
assert isinstance(ціна, float) and isinstance(кількість, int)
🟡 Рівень 2 — Плюс¶
Склади з цих даних однорядковий чек шириною рівно 46 символів: назва притиснута ліворуч, сума — праворуч із двома знаками після коми й роздільником тисяч.
Зроблено, якщо: len(чек) == 46 і в рядку видно 2 500.00 у вигляді
2,500.00, а між назвою та сумою — суцільні крапки-заповнювачі.
🔴 Рівень 3 — Виклик¶
Напиши функцію розвернути_слова(текст), яка розвертає порядок слів,
але не самі слова: "кава мелена мелена" → "мелена мелена кава".
Використай лише split, зрізи й join — без reversed і без циклів.
Зроблено, якщо: виконуються всі три перевірки
assert розвернути_слова("кава мелена мелена") == "мелена мелена кава"
assert розвернути_слова("одне") == "одне"
assert розвернути_слова(" зайві пробіли ") == "пробіли зайві"
Повні умови з підказками — у homework.md.