Text up close
Problem
The report that drags
You already know that title.upper() does nothing on its own: a string is immutable, and the method returns a new string. This has a cost, and you can see it in a loop.
The online store's program builds the daily report line by line:
report = ""for i in range(1, 4): report += f"Order {i}: €12.50\n"print(report, end="")Order 1: €12.50Order 2: €12.50Order 3: €12.50With three orders it is instant. On Saturday there are 50,000 orders, and the same loop takes about 1 second. We measured it on the machine where we check the code of the lessons.
Why? Each report += ... does not add to the old string. It makes a new string, copies the whole report so far into it, and only then glues on the new line. Near the end, every iteration copies almost a million characters.
The idea
A list of pieces and join
Python has no string that changes in place. So you collect the pieces in a list, which does change in place. At the end, join glues them into one string and copies each character only once:
parts = []for i in range(1, 4): parts.append(f"Order {i}: €12.50")report = "\n".join(parts)print(report)Order 1: €12.50Order 2: €12.50Order 3: €12.50parts = []is an empty list.appendadds a piece to the end without touching the others."\n".join(parts)makes one string from all the pieces, with the separator"\n"between them. The separator is the text before.join. To glue with no separator, you write"".join(parts).- The new line between the pieces comes from the separator, so here the pieces have no
\nat the end.
With 50,000 orders this version takes about 6 milliseconds instead of 1 second.
Here is what happens in memory with each approach:
report = ""for i in range(1, 4): report += f"Order {i}: €12.50\n"parts = []for i in range(1, 4): parts.append(f"Order {i}: €12.50\n")joined = "".join(parts)print(len(report), len(joined))print(report == joined)Step by step, in words
- report points to an empty string.
- The first += makes a new str with one line: 16 characters.
- The second += makes one more str. It copies the old line into it (16 characters) and only then glues on the new one. Nobody needs the old string anymore.
- The third copies 32 characters. Every line adds to the copying: with 50,000 lines it is almost a million characters per iteration.
- parts is an empty list: the pieces will go in it.
- append puts each piece in the list in place. The old pieces are not copied, and the list stays the same.
- join glues the pieces once: 48 characters. The text is the same. With += another 48 old characters were copied, and with join not a single one.
More precisely, if you are curious
- The same
+=loop inside a function takes about 4 milliseconds. Python (CPython, the usual version) cheats: if nothing else points to the old string, it extends it in place. At the top level of the file that does not happen. The trick is not a rule of the language, so do not rely on it: for many pieces, usejoin. - A single expression like
f"Order {i}: {price}"is one new string. The problem is only+=in a loop, where every iteration is a separate new string.
See it
Changing with slices
A string does not change in place, so every change is a new string, built from parts of the old one. Here is a card number for printing on a receipt:
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("stressed"[::-1])4532 0151 1283 0366**** **** **** 03660888123456dessertscard[:12] is a slice: the characters from the start up to index 12, without it. card[12:] is from index 12 to the end.
| You want to | You write |
|---|---|
insert x before index i |
s[:i] + x + s[i:] |
replace the characters from a to b (without b) with x |
s[:a] + x + s[b:] |
remove the character at index i |
s[:i] + s[i + 1:] |
remove the characters from a to b (without b) |
s[:a] + s[b:] |
| reverse the text, back to front | s[::-1] |
| get the length, a character | len(s), s[i] |
The spaces in the card number go in from the back: first at 12, then at 8, then at 4. If you start at 4, the inserted space shifts everything after it, and 8 is no longer the place you want. The dashes in the phone number are removed from the back first for the same reason.
Try it
Two names, one object
What will the program print?
note = ["Table 4"]copy = notecopy.append(", 2 coffees")print("".join(note))
text = "Table 4"other = textother += ", 2 coffees"print(text)copy = note copies the arrow, both variables point to the same list, and append changes it in place. A string is different: other += ... makes a new string and moves only other to it. text still points to the old one.The idea
In alphabetical order: < and >
== only tells you "are they the same". To sort, you need more: which string comes before the other. In Python you compare strings directly with < and >:
print("Andreev" < "Borisov")print("Borisov" == "Borisov")print("ivanova" < "Ivanova")print("ivanova".casefold() == "Ivanova".casefold())TrueTrueFalseTruea < bisTrueifacomes beforeb;a == b: they are the same;a > b:acomes afterb.
Capital letters come before lowercase ones, so "ivanova" < "Ivanova" is False. casefold() makes the text lowercase for comparing, no matter how it is written. With > you sort a list of names, for example with the bubble sort from the lesson on arrays:
names = ["Petrova", "Andreev", "Ivanova", "Borisov"]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))Andreev, Borisov, Ivanova, Petrova", ".join(names) glues the elements of the list with a separator between them.
More precisely, if you are curious
<and>compare character by character, by their codes in the character table.ordgives the code of a character: "i" and "I" differ by 32.- Python has no method that returns a negative number, 0 or a positive one, like
compareToin Java. You do not need it:<,==and>tell you the same thing. names.sort()sorts the list by the same rule, without writing the bubble sort.
print(ord("i") - ord("I"))names = ["Petrova", "Andreev", "Ivanova", "Borisov"]names.sort()print(", ".join(names))32Andreev, Borisov, Ivanova, PetrovaTry it
Invoice 10 or 9?
The invoice numbers are text. Which one is earlier?
What will the program print?
a = "INV-10"b = "INV-9"print(a < b)print(int(a[4:]) < int(b[4:]))Text is compared character by character. The first four characters are the same, then come "1" and "9", and "1" comes before "9". So "INV-10" comes before "INV-9", just as "ba" comes before "c" in a dictionary. The numbers 10 and 9 are compared as numbers only after int(). If you sort numbers as text, pad them with zeros: INV-009, INV-010.
The idea
Search, cut, put together
Here is a line from a contacts file: name, email and city, separated with " ; ", with extra spaces at the ends:
line = " Elena Koleva ; elena.koleva@company.com ; Plovdiv "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.kolevacompany.com20Elena Koleva | elena.koleva@company.com | Plovdiv====================strip()removes the spaces at the front and at the back, and also the "wide" spaces from other languages.find("@")gives the index of the first occurrence, andfind(".", at)starts searching only from indexaton.rfindsearches from the back. If there is no such character,findreturns -1.email[:at]is a slice: from the start up toat, without it.email[at + 1:]is fromat + 1to the end.joinis the opposite ofsplit."=" * 20repeats the text 20 times.
Empty and "blank" are not the same thing:
empty = ""spaces = " "print(empty == "", spaces == "")print(empty.strip() == "", spaces.strip() == "")True FalseTrue True== "" is true only for text of length 0. strip() == "" is also true when there are only spaces. To check a field someone filled in, you usually want the second one.
More precisely, if you are curious
indexsearches likefind, but when there is no such character it stops the program withValueErrorinstead of returning -1.isspace()looks like the check you need, but for empty text it isFalse: it is true only when there is at least one character and all of them are spaces.
email = "elena.koleva@company.com"print(email.find("#"))print("".isspace(), " ".isspace())try: email.index("#")except ValueError as e: print(e)-1False Truesubstring not foundThe idea
Numbers and text
Numbers go into text and come out of text:
code = int("0042")price = float("2.40")label = str(code) + "-" + str(price)print(label)42-2.4You already know int() and float(). str() is the opposite: it makes text from a number or a bool.
For a table with columns you need an f-string with a width for each column. Here is a receipt from a café:
names = ["Coffee", "Croissant", "Mineral water"]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("{:<29}€{:5.2f}".format("Total", total))Coffee 2 x 2.40 = 4.80Croissant 1 x 3.10 = 3.10Mineral water 3 x 1.50 = 4.50-----------------------------------Total €12.40{names[i]:<16}is text in 16 characters, aligned left.{prices[i]:5.2f}is a number in 5 characters with 2 after the point, aligned right.{qty[i]:2d}is a whole number in 2 characters."template".format(...)does the same with empty{}in the template: the values come from outside, in order. This is handy when the template is ready beforehand and the values come later.
More precisely, if you are curious
- The width is at least that many characters. Longer text is not cut. It pushes the column to the right.
float("2,40")stops withValueError: the decimal mark is always a point. And the f-string always writes a point, no matter what the computer's settings are.
try: float("2,40")except ValueError as e: print(e)print(f"{2.4:.2f}")could not convert string to float: '2,40'2.40Try it
Find the bug
"Kayak" reads the same backwards, but the program says "kayak is not a palindrome". Which line has the bug?
Pick the line with the bug.
reversed(word) does not return a string. It returns an object that gives the characters back to front, one by one. == between a string and such an object is False, whatever is inside. The fix is "".join(reversed(word)), or shorter, word[::-1].Exercise
Text you put together
Solve the tasks in your editor. Do not print prompts.
1. Palindrome. Read a line and say whether it is a palindrome, ignoring capital letters, spaces, commas and dashes. This time reverse the text with the slice [::-1] instead of a loop.
Never odd or evenPalindromeNever odd, never evenNot a palindrome2. A receipt with columns. The first line has the number of items. Each line after it has a name, a price and a quantity, separated with ;. Print a receipt with aligned columns and the total at the end.
3Bread;1.20;2Cheese;9.80;1Eggs;0.35;10Bread 2 x 1.20 = 2.40Cheese 1 x 9.80 = 9.80Eggs 10 x 0.35 = 3.50------------------------------------Total 15.703. Words in reverse. Read a sentence and print its words in reverse order. There may be more than one space between the words, and the result must have exactly one. Put it together with a list and join.
Tomorrow at 9 there is a meetingmeeting a is there 9 at Tomorrowsplit() with no argument splits on every run of spaces and gives no empty parts. Try split(" ") too, to see the difference.
More tasks
- Initials. Read a full name ("Maria Ivanova Petrova") and print the initials with dots: "M. I. P." Put them together with a list and
join. - Hidden email. From
elena.koleva@company.commakee***********@company.com: the first letter, then as many asterisks as there are other characters before the@. Usefindand*. - A tour of str. Create one string with quotes (
"Practice text") and another glued from two parts with"".join(["Practice", " text"]). Compare them with==and withis, take the slice[0:8], split the second one on the space and print the parts, turn"100"into a number and print it withf"Value: {...}", and finally add" and more"to a list with the first string and glue them withjoin. Predict every line before you run the program. - Sort ignoring case. Change the bubble sort from the comparing card to compare the
casefold()of the two strings, and try it with "ivan", "Boris" and "ana".
Test