Текстът отблизо
Проблем
Отчетът, който се бави
Вече знаеш, че 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)Стъпка по стъпка, с думи
- report сочи празен текст.
- Първото += прави нов str с един ред: 19 знака.
- Второто += прави още един str. Копира в него стария ред (19 знака) и чак тогава долепва новия. Старият текст вече не трябва на никого.
- Третото копира 38 знака. С всеки ред копирането става повече: при 50 000 реда е над милион знака на завъртане.
- parts е празен списък: в него ще стоят парчетата.
- append слага всяко парче в списъка на място. Старите парчета не се копират, списъкът е същият.
- 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**** **** **** 03660888123456ефак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 = notecopy.append(", 2 кафета")print("".join(note))
text = "Маса 4"other = textother += ", 2 кафета"print(text)copy = note копира стрелката, двете променливи сочат един и същ списък, а append го променя на място. При текста е различно: other += ... прави нов текст и мести само other към него. text си сочи стария.Идеята
По азбучен ред: < и >
== казва само „еднакви ли са“. За подреждане трябва повече: кой текст е преди другия. В Python текстовете се сравняват направо с < и >:
print("Андреев" < "Борисов")print("Борисов" == "Борисов")print("иванова" < "Иванова")print("иванова".casefold() == "Иванова".casefold())TrueTrueFalseTruea < 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] = tmpprint(", ".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:]))Текстът се сравнява символ по символ. Първите четири символа са еднакви, после идват „1“ и „9“, а „1“ е преди „9“. Затова "INV-10" е преди "INV-9", точно както в речника „ба“ е преди „в“. Числата 10 и 9 се сравняват като числа едва след int(). Ако подреждаш номера като текст, допълвай с нули: INV-009, INV-010.
Идеята
Търсене, рязане, сглобяване
Ред от файл с контакти: име, имейл и град, разделени с „ ; “, с излишни интервали по краищата:
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.kolevafirma.bg18Елена Колева | 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 FalseTrue 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)-1False Truesubstring not foundИдеята
Числа и текст
Числата влизат в текст и излизат от текст:
code = int("0042")price = float("2.40")label = str(code) + "-" + str(price)print(label)42-2.4int() и float() вече познаваш. str() е обратното: прави текст от число или bool.
За таблица с колони ти трябва f-низ с ширина на всяка колона. Бележка от кафене:
names = ["Кафе", "Кроасан", "Минерална вода"]prices = [2.40, 3.10, 1.50]qty = [2, 1, 3]total = 0for 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Пробвай
Намери грешката
„Капак“ се чете еднакво и отзад напред, а програмата казва „капак не е палиндром“. На кой ред е грешката?
Избери реда с грешката.
reversed(word) не връща текст, а обект, който дава символите отзад напред един по един. == между текст и такъв обект е False, каквото и да има вътре. Поправката е "".join(reversed(word)) или по-кратко word[::-1].Упражнение
Текст, който сглобяваш
Реши задачите в редактора си. Не печатай въпроси.
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.703. Думите наобратно. Прочети изречение и отпечатай думите му в обратен ред. Между думите може да има повече от един интервал, а в резултата трябва да има точно един. Сглоби го със списък и join.
Утре в 9 ще има срещасреща има ще 9 в Утреsplit() без аргумент разделя по всяка поредица от интервали и не дава празни части. Пробвай и split(" "), за да видиш разликата.
Още задачи
- Инициали. Прочети пълно име („Мария Иванова Петрова“) и отпечатай инициалите с точки: „М. И. П.“ Сглоби ги със списък и
join. - Скрит имейл. От
elena.koleva@firma.bgнаправиe***********@firma.bg: първата буква, после толкова звездички, колкото са останалите символи преди@. Ползвайfindи*. - Обиколка на str. Създай текст с кавички (
"Примерен текст") и друг, слепен от две части с"".join(["Примерен", " текст"]). Сравни ги с==и сis, вземи резена[0:8], раздели втория по интервала и отпечатай частите, превърни"100"в число и го отпечатай сf"Стойност: {...}", и накрая добави" и още"към списък с първия текст и ги слепи сjoin. Предвиди всеки ред, преди да пуснеш програмата. - Подреди без значение от главните букви. Промени мехурчето от картата за сравняване да сравнява
casefold()на двата текста и пробвай с „иван“, „Борис“ и „ана“.
Тест