Strings
Kurz gesagt: Ein String in Python ändert sich nicht: Jedes += macht einen neuen Text, deshalb setzt man viele Stücke mit einer Liste und join zusammen.
Problem
Der Bericht, der sich hinzieht
Du weißt schon, dass title.upper() allein nichts bewirkt: Ein String ist unveränderlich, und die Methode gibt einen neuen String zurück. Das hat einen Preis, und in einer Schleife sieht man ihn.
Das Programm des Onlineshops baut den Tagesbericht Zeile für Zeile zusammen:
report = ""for i in range(1, 4): report += f"Auftrag {i}: 12.50 €\n"print(report, end="")Auftrag 1: 12.50 €Auftrag 2: 12.50 €Auftrag 3: 12.50 €Mit drei Bestellungen geht es sofort. Am Samstag sind es 50.000 Bestellungen, und dieselbe Schleife braucht etwa 6,5 Sekunden. Wir haben es auf dem Rechner gemessen, auf dem wir den Code der Lektionen prüfen.
Warum? Jedes report += ... hängt nicht an den alten String an. Es macht einen neuen String, kopiert den ganzen bisherigen Bericht hinein und klebt erst dann die neue Zeile an. Gegen Ende kopiert jeder Durchlauf über eine Million Zeichen.
Die Idee
Eine Liste von Stücken und join
Python hat keinen String, der sich an Ort und Stelle ändert. Deshalb sammelst du die Stücke in einer Liste, und die ändert sich an Ort und Stelle. Am Ende klebt join sie zu einem einzigen String zusammen und kopiert jedes Zeichen nur einmal:
parts = []for i in range(1, 4): parts.append(f"Auftrag {i}: 12.50 €")report = "\n".join(parts)print(report)Auftrag 1: 12.50 €Auftrag 2: 12.50 €Auftrag 3: 12.50 €parts = []ist eine leere Liste.appendhängt ein Stück ans Ende, ohne die anderen anzufassen."\n".join(parts)macht einen String aus allen Stücken, mit dem Trennzeichen"\n"dazwischen. Das Trennzeichen ist der Text vor.join. Zum Zusammenkleben ohne Trennzeichen schreibst du"".join(parts).- Die neue Zeile zwischen den Stücken kommt vom Trennzeichen, deshalb haben die Stücke hier kein
\nam Ende.
Mit 50.000 Bestellungen braucht diese Variante etwa 6 Millisekunden statt 6,5 Sekunden.
So sieht es bei beiden Wegen im Speicher aus:
report = ""for i in range(1, 4): report += f"Auftrag {i}: 12.50 €\n"parts = []for i in range(1, 4): parts.append(f"Auftrag {i}: 12.50 €\n")joined = "".join(parts)print(len(report), len(joined))print(report == joined)Schritt für Schritt, in Worten
- report zeigt auf einen leeren String.
- Das erste += macht einen neuen str mit einer Zeile: 19 Zeichen.
- Das zweite += macht noch einen str. Es kopiert die alte Zeile hinein (19 Zeichen) und hängt erst dann die neue an. Den alten String braucht niemand mehr.
- Das dritte kopiert 38 Zeichen. Mit jeder Zeile wird mehr kopiert: bei 50.000 Zeilen sind es über eine Million Zeichen pro Durchlauf.
- parts ist eine leere Liste: Darin landen die Stücke.
- append legt jedes Stück direkt in die Liste. Die alten Stücke werden nicht kopiert, die Liste bleibt dieselbe.
- join klebt die Stücke einmal zusammen: 57 Zeichen. Der Text ist derselbe. Mit += wurden zusätzlich 57 alte Zeichen kopiert, mit join kein einziges.
Genauer, für Neugierige
- Dieselbe
+=-Schleife in einer Funktion braucht etwa 4 Millisekunden. Python (CPython, die übliche Version) schummelt: Wenn nichts anderes auf den alten String zeigt, verlängert es ihn an Ort und Stelle. Auf der obersten Ebene der Datei passiert das nicht. Der Trick ist keine Regel der Sprache, also verlass dich nicht darauf: Für viele Stücke nimmjoin. - Ein einzelner Ausdruck wie
f"Auftrag {i}: {price}"ist ein einziger neuer String. Das Problem ist nur+=in einer Schleife, wo jeder Durchlauf ein eigener neuer String ist.
Sieh es dir an
Ändern mit Slices
Ein String ändert sich nicht an Ort und Stelle, deshalb ist jede Änderung ein neuer String, gebaut aus Teilen des alten. Hier ist eine Kartennummer für den Druck auf dem Kassenbon:
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("regal"[::-1])4532 0151 1283 0366**** **** **** 03660888123456lagercard[:12] ist ein Slice (Ausschnitt): die Zeichen vom Anfang bis zum Index 12, ohne ihn. card[12:] geht vom Index 12 bis zum Ende.
| Du willst | Du schreibst |
|---|---|
x vor dem Index i einfügen |
s[:i] + x + s[i:] |
die Zeichen von a bis b (ohne b) durch x ersetzen |
s[:a] + x + s[b:] |
das Zeichen mit dem Index i entfernen |
s[:i] + s[i + 1:] |
die Zeichen von a bis b (ohne b) entfernen |
s[:a] + s[b:] |
| den Text umdrehen, von hinten nach vorne | s[::-1] |
| die Länge, ein Zeichen | len(s), s[i] |
Die Leerzeichen in der Kartennummer kommen von hinten hinein: zuerst bei 12, dann bei 8, dann bei 4. Wenn du bei 4 anfängst, verschiebt das eingefügte Leerzeichen alles dahinter, und 8 ist nicht mehr die Stelle, die du willst. Die Bindestriche in der Telefonnummer werden aus demselben Grund zuerst von hinten entfernt.
Probier es aus
Zwei Namen, ein Objekt
Was gibt das Programm aus?
note = ["Tisch 4"]copy = notecopy.append(", 2 Kaffee")print("".join(note))
text = "Tisch 4"other = textother += ", 2 Kaffee"print(text)copy = note kopiert den Pfeil, beide Variablen zeigen auf dieselbe Liste, und append ändert sie direkt. Bei einem String ist es anders: other += ... macht einen neuen String und lenkt nur other darauf um. text zeigt weiter auf den alten.Die Idee
In alphabetischer Reihenfolge: < und >
== sagt dir nur „sind sie gleich“. Zum Sortieren brauchst du mehr: welcher String vor dem anderen kommt. In Python vergleichst du Strings direkt mit < und >:
print("Albrecht" < "Bauer")print("Bauer" == "Bauer")print("neumann" < "Neumann")print("neumann".casefold() == "Neumann".casefold())TrueTrueFalseTruea < bistTrue, wennavorbkommt;a == b: Sie sind gleich;a > b:akommt nachb.
Großbuchstaben kommen vor Kleinbuchstaben, deshalb ist "neumann" < "Neumann" False. casefold() macht den Text zum Vergleichen klein, egal wie er geschrieben ist. Mit > sortierst du eine Liste von Namen, zum Beispiel mit dem Bubblesort aus der Lektion „Arrays“:
names = ["Weber", "Albrecht", "Neumann", "Bauer"]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))Albrecht, Bauer, Neumann, Weber", ".join(names) klebt die Elemente der Liste mit einem Trennzeichen dazwischen zusammen.
Genauer, für Neugierige
<und>vergleichen Zeichen für Zeichen, nach ihren Codes in der Zeichentabelle.ordgibt den Code eines Zeichens: „n“ und „N“ unterscheiden sich um 32.- Python hat keine Methode, die eine negative Zahl, 0 oder eine positive zurückgibt, wie
compareToin Java. Du brauchst sie nicht:<,==und>sagen dasselbe. names.sort()sortiert die Liste nach derselben Regel, ohne dass du den Bubblesort schreibst.
print(ord("n") - ord("N"))names = ["Weber", "Albrecht", "Neumann", "Bauer"]names.sort()print(", ".join(names))32Albrecht, Bauer, Neumann, WeberProbier es aus
Rechnung 10 oder 9?
Die Rechnungsnummern sind Text. Welche ist früher?
Was gibt das Programm aus?
a = "INV-10"b = "INV-9"print(a < b)print(int(a[4:]) < int(b[4:]))Text wird Zeichen für Zeichen verglichen. Die ersten vier Zeichen sind gleich, dann kommen „1“ und „9“, und „1“ kommt vor „9“. Deshalb kommt "INV-10" vor "INV-9", genau wie „ba“ im Wörterbuch vor „c“ kommt. Die Zahlen 10 und 9 werden erst nach int() als Zahlen verglichen. Wenn du Nummern als Text sortierst, fülle sie mit Nullen auf: INV-009, INV-010.
Die Idee
Suchen, zerschneiden, zusammensetzen
Hier ist eine Zeile aus einer Kontaktdatei: Name, E-Mail und Stadt, getrennt durch „ ; “, mit überflüssigen Leerzeichen an den Enden:
line = " Maria Becker ; maria.becker@firma.de ; Köln "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)maria.beckerfirma.de18Maria Becker | maria.becker@firma.de | Köln====================strip()entfernt die Leerzeichen vorne und hinten und auch die „breiten“ Leerzeichen aus anderen Sprachen.find("@")gibt den Index des ersten Vorkommens, undfind(".", at)sucht erst ab dem Indexat.rfindsucht von hinten. Wenn es kein solches Zeichen gibt, gibtfind-1 zurück.email[:at]ist ein Slice: vom Anfang bisat, ohne es.email[at + 1:]geht vonat + 1bis zum Ende.joinist das Gegenteil vonsplit."=" * 20wiederholt den Text 20-mal.
Leer und „leer aussehend“ sind nicht dasselbe:
empty = ""spaces = " "print(empty == "", spaces == "")print(empty.strip() == "", spaces.strip() == "")True FalseTrue True== "" ist nur für Text der Länge 0 wahr. strip() == "" ist auch wahr, wenn nur Leerzeichen darin stehen. Um ein Feld zu prüfen, das jemand ausgefüllt hat, willst du meistens das Zweite.
Genauer, für Neugierige
indexsucht wiefind, aber wenn es kein solches Zeichen gibt, hält es das Programm mitValueErroran, statt -1 zurückzugeben.isspace()sieht aus wie die Prüfung, die du brauchst, aber für leeren Text ist esFalse: Es ist nur wahr, wenn es mindestens ein Zeichen gibt und alle Leerzeichen sind.
email = "maria.becker@firma.de"print(email.find("#"))print("".isspace(), " ".isspace())try: email.index("#")except ValueError as e: print(e)-1False Truesubstring not foundDie Idee
Zahlen und Text
Zahlen kommen in Text hinein und aus Text heraus:
code = int("0042")price = float("2.40")label = str(code) + "-" + str(price)print(label)42-2.4int() und float() kennst du schon. str() ist das Gegenteil: Es macht Text aus einer Zahl oder einem bool.
Für eine Tabelle mit Spalten brauchst du einen f-String mit einer Breite für jede Spalte. Hier ist ein Kassenbon aus einem Café:
names = ["Kaffee", "Croissant", "Mineralwasser"]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("Gesamt", total))Kaffee 2 x 2.40 = 4.80Croissant 1 x 3.10 = 3.10Mineralwasser 3 x 1.50 = 4.50-----------------------------------Gesamt 12.40 €{names[i]:<16}ist Text in 16 Zeichen, linksbündig.{prices[i]:5.2f}ist eine Zahl in 5 Zeichen mit 2 Stellen nach dem Punkt, rechtsbündig.{qty[i]:2d}ist eine ganze Zahl in 2 Zeichen."vorlage".format(...)macht dasselbe mit leeren{}in der Vorlage: Die Werte kommen von außen, der Reihe nach. Das ist praktisch, wenn die Vorlage vorher fertig ist und die Werte später kommen.
Genauer, für Neugierige
- Die Breite ist mindestens so viele Zeichen. Längerer Text wird nicht abgeschnitten. Er schiebt die Spalte nach rechts.
float("2,40")hält mitValueErroran: Das Dezimalzeichen ist immer ein Punkt. Und der f-String schreibt immer einen Punkt, egal welche Einstellungen der Computer hat.
try: float("2,40")except ValueError as e: print(e)print(f"{2.4:.2f}")could not convert string to float: '2,40'2.40Probier es aus
Finde den Fehler
„neben“ liest sich rückwärts genauso, aber das Programm sagt „neben ist kein Palindrom“. In welcher Zeile ist der Fehler?
Wähle die Zeile mit dem Fehler.
reversed(word) gibt keinen String zurück. Es gibt ein Objekt zurück, das die Zeichen von hinten nach vorne liefert, eins nach dem anderen. == zwischen einem String und so einem Objekt ist False, egal was darin steht. Die Korrektur ist "".join(reversed(word)) oder kürzer word[::-1].Übung
Text, den du zusammensetzt
Löse die Aufgaben in deinem Editor. Gib keine Eingabeaufforderungen aus.
1. Palindrom. Lies eine Zeile und sag, ob sie ein Palindrom ist, ohne auf Großbuchstaben, Leerzeichen, Kommas und Bindestriche zu achten. Dreh den Text diesmal mit dem Slice [::-1] um statt mit einer Schleife.
Erika feuert nur untreue FakirePalindromErika feuert nur treue FakireKein Palindrom2. Ein Kassenbon mit Spalten. In der ersten Zeile steht die Anzahl der Artikel. In jeder Zeile danach stehen ein Name, ein Preis und eine Menge, getrennt durch ;. Gib einen Kassenbon mit ausgerichteten Spalten und der Summe am Ende aus.
3Brot;1.20;2Käse;9.80;1Eier;0.35;10Brot 2 x 1.20 = 2.40Käse 1 x 9.80 = 9.80Eier 10 x 0.35 = 3.50------------------------------------Gesamt 15.703. Wörter rückwärts. Lies einen Satz und gib seine Wörter in umgekehrter Reihenfolge aus. Zwischen den Wörtern kann mehr als ein Leerzeichen stehen, und im Ergebnis muss genau eines stehen. Setz es mit einer Liste und join zusammen.
Morgen um 9 ist eine BesprechungBesprechung eine ist 9 um Morgensplit() ohne Argument trennt bei jeder Folge von Leerzeichen und liefert keine leeren Teile. Probier auch split(" "), um den Unterschied zu sehen.
Weitere Aufgaben
- Initialen. Lies einen vollen Namen („Maria Luise Becker“) und gib die Initialen mit Punkten aus: „M. L. B.“ Setz sie mit einer Liste und
joinzusammen. - Versteckte E-Mail. Mach aus
maria.becker@firma.dedie Adressem***********@firma.de: der erste Buchstabe, dann so viele Sternchen, wie es andere Zeichen vor dem@gibt. Nimmfindund*. - Eine Runde durch str. Erzeuge einen String mit Anführungszeichen (
"Beispiel eins") und einen anderen, aus zwei Teilen zusammengeklebt mit"".join(["Beispiel", " eins"]). Vergleiche sie mit==und mitis, nimm den Slice[0:8], trenne den zweiten beim Leerzeichen und gib die Teile aus, mach aus"100"eine Zahl und gib sie mitf"Wert: {...}"aus, und füge schließlich" und mehr"zu einer Liste mit dem ersten String hinzu und klebe sie mitjoinzusammen. Sag jede Zeile voraus, bevor du das Programm startest. - Sortieren ohne Groß- und Kleinschreibung. Ändere den Bubblesort von der Karte zum Vergleichen so, dass er das
casefold()der beiden Strings vergleicht, und probier ihn mit „lena“, „Jonas“ und „anna“ aus.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Strings: mit Text arbeiten · Klassen und Objekte · Instanzmethoden · Vererbung
- Geht weiter in: Reguläre Ausdrücke · Reguläre Ausdrücke: Gruppen und Ersetzen · Dateien · Daten in Dateien
- Aufgaben zum Thema: Text und Dateien