Към съдържанието
wannacode

Текстът отблизо

Проблем

Отчетът, който се бави

Вече знаеш, че title.upper() сам не прави нищо: текстът е неизменим и методът връща нов текст. Това има цена, която се вижда в цикъл.

Програмата на онлайн магазина сглобява дневния отчет ред по ред:

report = ""
for i in range(1, 4):
report += f"Поръчка {i}: 12.50 €\n"
print(report, end="")
Изход
Поръчка 1: 12.50 €
Поръчка 2: 12.50 €
Поръчка 3: 12.50 €

С три поръчки е мигновено. В събота поръчките са 50 000 и същият цикъл отнема около 6,5 секунди. Измерихме го на машината, на която проверяваме кода на уроците.

Защо? Всяко report += ... не добавя към стария текст. То прави нов текст, копира в него целия отчет досега и чак тогава долепва новия ред. Към края всяко завъртане копира над милион символа.

Идеята

Списък от парчета и join

В Python няма текст, който се променя на място. Затова парчетата се събират в списък, а той се променя на място. Накрая join ги слепва в един текст, като копира всеки символ само веднъж:

parts = []
for i in range(1, 4):
parts.append(f"Поръчка {i}: 12.50 €")
report = "\n".join(parts)
print(report)
Изход
Поръчка 1: 12.50 €
Поръчка 2: 12.50 €
Поръчка 3: 12.50 €
  • parts = [] е празен списък. append добавя парче накрая, без да пипа другите.
  • "\n".join(parts) прави един текст от всички парчета, с разделителя "\n" между тях. Разделителят е текстът пред .join. За слепване без разделител пишеш "".join(parts).
  • Новият ред между парчетата идва от разделителя, затова тук парчетата нямат \n накрая.

Със 50 000 поръчки този вариант отнема около 6 милисекунди вместо 6,5 секунди.

Ето какво става в паметта при двата начина:

report = ""
for i in range(1, 4):
report += f"Поръчка {i}: 12.50 €\n"
parts = []
for i in range(1, 4):
parts.append(f"Поръчка {i}: 12.50 €\n")
joined = "".join(parts)
print(len(report), len(joined))
print(report == joined)
Стъпка по стъпка, с думи
  1. report сочи празен текст.
  2. Първото += прави нов str с един ред: 19 знака.
  3. Второто += прави още един str. Копира в него стария ред (19 знака) и чак тогава долепва новия. Старият текст вече не трябва на никого.
  4. Третото копира 38 знака. С всеки ред копирането става повече: при 50 000 реда е над милион знака на завъртане.
  5. parts е празен списък: в него ще стоят парчетата.
  6. append слага всяко парче в списъка на място. Старите парчета не се копират, списъкът е същият.
  7. join слепва парчетата веднъж: 57 знака. Текстът е един и същ. С += бяха копирани още 57 стари знака, а с join нито един.
По-точно, за любопитните
  • Същият цикъл с += във функция отнема около 4 милисекунди. Python (CPython, обичайната версия) хитрува: ако никой друг не сочи стария текст, той го удължава на място. На най-горно ниво на файла това не става. Хитрината не е правило на езика, затова не разчитай на нея: за много парчета ползвай join.
  • Един израз като f"Поръчка {i}: {price}" е един нов текст. Проблемът е само в += в цикъл, където всяко завъртане е отделен нов текст.

Виж го

Промяна с резени

Текстът не се променя на място, затова всяка промяна е нов текст, сглобен от части на стария. Номер на карта за печат на касовата бележка:

card = "4532015112830366"
card = card[:12] + " " + card[12:]
card = card[:8] + " " + card[8:]
card = card[:4] + " " + card[4:]
print(card)
card = "**** **** ****" + card[14:]
print(card)
phone = "0888-123-456"
phone = phone[:8] + phone[9:]
phone = phone[:4] + phone[5:]
print(phone)
print("кафе"[::-1])
Изход
4532 0151 1283 0366
**** **** **** 0366
0888123456
ефак

card[:12] е резен (slice): символите от началото до индекс 12, без него. card[12:] е от индекс 12 до края.

Искаш Пишеш
вмъкни x преди индекс i s[:i] + x + s[i:]
замени символите от a до b (без b) с x s[:a] + x + s[b:]
махни символа с индекс i s[:i] + s[i + 1:]
махни символите от a до b (без b) s[:a] + s[b:]
обърни текста отзад напред s[::-1]
дължина, символ len(s), s[i]

Интервалите в номера на картата се вмъкват отзад напред: първо на 12, после на 8, после на 4. Ако започнеш от 4, вмъкнатият интервал измества всичко след него и 8 вече не е мястото, което искаш. Тиретата в телефона се махат по същата причина първо отзад.

Пробвай

Две имена, един обект

Какво ще отпечата програмата?

note = ["Маса 4"]
copy = note
copy.append(", 2 кафета")
print("".join(note))
text = "Маса 4"
other = text
other += ", 2 кафета"
print(text)
Отговор

Идеята

По азбучен ред: < и >

== казва само „еднакви ли са“. За подреждане трябва повече: кой текст е преди другия. В Python текстовете се сравняват направо с < и >:

print("Андреев" < "Борисов")
print("Борисов" == "Борисов")
print("иванова" < "Иванова")
print("иванова".casefold() == "Иванова".casefold())
Изход
True
True
False
True
  • a < b е True, ако a е преди b;
  • a == b: еднакви са;
  • a > b: a е след b.

Главните букви са преди малките, затова "иванова" < "Иванова" е False. casefold() прави текста с малки букви за сравнение, без значение как е написан. С > подреждаш списък с имена, например с метода на мехурчето от урока за масивите:

names = ["Петрова", "Андреев", "Иванова", "Борисов"]
for i in range(len(names) - 1):
for j in range(len(names) - 1 - i):
if names[j] > names[j + 1]:
tmp = names[j]
names[j] = names[j + 1]
names[j + 1] = tmp
print(", ".join(names))
Изход
Андреев, Борисов, Иванова, Петрова

", ".join(names) долепва елементите на списъка с разделител между тях.

По-точно, за любопитните
  • < и > сравняват символ по символ по кода им в таблицата със символи. ord дава кода на символа: „и“ и „И“ се различават с 32.
  • В Python няма метод, който връща отрицателно число, 0 или положително, като compareTo в Java. Не ти трябва: <, == и > казват същото.
  • names.sort() подрежда списъка по същото правило, без да пишеш мехурчето.
print(ord("и") - ord("И"))
names = ["Петрова", "Андреев", "Иванова", "Борисов"]
names.sort()
print(", ".join(names))
Изход
32
Андреев, Борисов, Иванова, Петрова

Пробвай

Фактура 10 или 9?

Номерата на фактурите са текст. Коя е по-ранна?

Какво ще отпечата програмата?

a = "INV-10"
b = "INV-9"
print(a < b)
print(int(a[4:]) < int(b[4:]))
Отговор

Идеята

Търсене, рязане, сглобяване

Ред от файл с контакти: име, имейл и град, разделени с „ ; “, с излишни интервали по краищата:

line = " Елена Колева ; elena.koleva@firma.bg ; Пловдив "
parts = line.strip().split(" ; ")
email = parts[1]
at = email.find("@")
print(email[:at])
print(email[at + 1:])
print(email.find(".", at))
print(" | ".join(parts))
print("=" * 20)
Изход
elena.koleva
firma.bg
18
Елена Колева | elena.koleva@firma.bg | Пловдив
====================
  • strip() маха интервалите отпред и отзад, и „широките“ интервали от другите езици.
  • find("@") дава индекса на първата поява, а find(".", at) търси чак от индекс at нататък. rfind търси отзад напред. Ако няма такъв символ, find връща -1.
  • email[:at] е резен: от началото до at, без него. email[at + 1:] е от at + 1 до края. join е обратното на split.
  • "=" * 20 повтаря текста 20 пъти.

Празно и „празно“ не е едно и също:

empty = ""
spaces = " "
print(empty == "", spaces == "")
print(empty.strip() == "", spaces.strip() == "")
Изход
True False
True True

== "" е вярно само за текст с дължина 0. strip() == "" е вярно и когато има само интервали. За проверка на въведено поле обикновено искаш второто.

По-точно, за любопитните
  • index търси като find, но когато няма такъв символ, спира програмата с ValueError вместо да върне -1.
  • isspace() изглежда като проверката, която ти трябва, но за празен текст е False: вярно е само когато има поне един символ и всички са интервали.
email = "elena.koleva@firma.bg"
print(email.find("#"))
print("".isspace(), " ".isspace())
try:
email.index("#")
except ValueError as e:
print(e)
Изход
-1
False True
substring not found

Идеята

Числа и текст

Числата влизат в текст и излизат от текст:

code = int("0042")
price = float("2.40")
label = str(code) + "-" + str(price)
print(label)
Изход
42-2.4

int() и float() вече познаваш. str() е обратното: прави текст от число или bool.

За таблица с колони ти трябва f-низ с ширина на всяка колона. Бележка от кафене:

names = ["Кафе", "Кроасан", "Минерална вода"]
prices = [2.40, 3.10, 1.50]
qty = [2, 1, 3]
total = 0
for i in range(len(names)):
amount = prices[i] * qty[i]
total += amount
print(f"{names[i]:<16}{qty[i]:2d} x {prices[i]:5.2f} = {amount:6.2f}")
print("-" * 35)
print("{:<26}{:9.2f} €".format("Общо", total))
Изход
Кафе 2 x 2.40 = 4.80
Кроасан 1 x 3.10 = 3.10
Минерална вода 3 x 1.50 = 4.50
-----------------------------------
Общо 12.40 €
  • {names[i]:<16} е текст в 16 знака, подравнен вляво. {prices[i]:5.2f} е число в 5 знака с 2 след точката, подравнено вдясно. {qty[i]:2d} е цяло число в 2 знака.
  • "шаблон".format(...) прави същото с празни {} в шаблона: стойностите идват отвън, по ред. Удобно е, когато шаблонът е готов отпреди, а стойностите идват после.
По-точно, за любопитните
  • Ширината е най-малко толкова знака. По-дълъг текст не се реже, а избутва колоната вдясно.
  • float("2,40") спира с ValueError: десетичният знак е винаги точка. И f-низът винаги пише точка, без значение от настройките на компютъра.
try:
float("2,40")
except ValueError as e:
print(e)
print(f"{2.4:.2f}")
Изход
could not convert string to float: '2,40'
2.40

Пробвай

Намери грешката

„Капак“ се чете еднакво и отзад напред, а програмата казва „капак не е палиндром“. На кой ред е грешката?

Избери реда с грешката.

Упражнение

Текст, който сглобяваш

Реши задачите в редактора си. Не печатай въпроси.

1. Палиндром. Прочети ред и кажи дали е палиндром, без значение от главните букви, интервалите, запетаите и тиретата. Този път обърни текста с резена [::-1] вместо с цикъл.

Вход
Дебел лебед
Изход
Палиндром
Вход
Дебел гълъб
Изход
Не е палиндром

2. Бележка с колони. На първия ред е броят на артикулите. На всеки следващ ред има име, цена и брой, разделени с ;. Отпечатай бележка с подравнени колони и сумата накрая.

Вход
3
Хляб;1.20;2
Сирене;9.80;1
Яйца;0.35;10
Изход
Хляб 2 x 1.20 = 2.40
Сирене 1 x 9.80 = 9.80
Яйца 10 x 0.35 = 3.50
------------------------------------
Общо 15.70

3. Думите наобратно. Прочети изречение и отпечатай думите му в обратен ред. Между думите може да има повече от един интервал, а в резултата трябва да има точно един. Сглоби го със списък и join.

Вход
Утре в 9 ще има среща
Изход
среща има ще 9 в Утре

split() без аргумент разделя по всяка поредица от интервали и не дава празни части. Пробвай и split(" "), за да видиш разликата.

Още задачи

  • Инициали. Прочети пълно име („Мария Иванова Петрова“) и отпечатай инициалите с точки: „М. И. П.“ Сглоби ги със списък и join.
  • Скрит имейл. От elena.koleva@firma.bg направи e***********@firma.bg: първата буква, после толкова звездички, колкото са останалите символи преди @. Ползвай find и *.
  • Обиколка на str. Създай текст с кавички ("Примерен текст") и друг, слепен от две части с "".join(["Примерен", " текст"]). Сравни ги с == и с is, вземи резена [0:8], раздели втория по интервала и отпечатай частите, превърни "100" в число и го отпечатай с f"Стойност: {...}", и накрая добави " и още" към списък с първия текст и ги слепи с join. Предвиди всеки ред, преди да пуснеш програмата.
  • Подреди без значение от главните букви. Промени мехурчето от картата за сравняване да сравнява casefold() на двата текста и пробвай с „иван“, „Борис“ и „ана“.

Тест

Провери се

1. Защо report += line в цикъл с 50 000 завъртания на най-горно ниво на файла е бавно?
2. Какво дава "Борисов" > "Андреев"?
3. a = ["12"]; b = a; b.append("3"). Какво дава "".join(a)?
4. parts е списък от текстове. Как правиш от него един текст?
5. Каква е разликата между f"{total:6.2f}" и "{:6.2f}".format(total)?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·