Zum Inhalt
wannacode

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="")
Ausgabe
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)
Ausgabe
Auftrag 1: 12.50 €
Auftrag 2: 12.50 €
Auftrag 3: 12.50 €
  • parts = [] ist eine leere Liste. append hä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 \n am 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
  1. report zeigt auf einen leeren String.
  2. Das erste += macht einen neuen str mit einer Zeile: 19 Zeichen.
  3. 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.
  4. Das dritte kopiert 38 Zeichen. Mit jeder Zeile wird mehr kopiert: bei 50.000 Zeilen sind es über eine Million Zeichen pro Durchlauf.
  5. parts ist eine leere Liste: Darin landen die Stücke.
  6. append legt jedes Stück direkt in die Liste. Die alten Stücke werden nicht kopiert, die Liste bleibt dieselbe.
  7. 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 nimm join.
  • 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])
Ausgabe
4532 0151 1283 0366
**** **** **** 0366
0888123456
lager

card[: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 = note
copy.append(", 2 Kaffee")
print("".join(note))
text = "Tisch 4"
other = text
other += ", 2 Kaffee"
print(text)
Antwort

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())
Ausgabe
True
True
False
True
  • a < b ist True, wenn a vor b kommt;
  • a == b: Sie sind gleich;
  • a > b: a kommt nach b.

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] = tmp
print(", ".join(names))
Ausgabe
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. ord gibt 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 compareTo in 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))
Ausgabe
32
Albrecht, Bauer, Neumann, Weber

Probier 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:]))
Antwort

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)
Ausgabe
maria.becker
firma.de
18
Maria 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, und find(".", at) sucht erst ab dem Index at. rfind sucht von hinten. Wenn es kein solches Zeichen gibt, gibt find -1 zurück.
  • email[:at] ist ein Slice: vom Anfang bis at, ohne es. email[at + 1:] geht von at + 1 bis zum Ende. join ist das Gegenteil von split.
  • "=" * 20 wiederholt den Text 20-mal.

Leer und „leer aussehend“ sind nicht dasselbe:

empty = ""
spaces = " "
print(empty == "", spaces == "")
print(empty.strip() == "", spaces.strip() == "")
Ausgabe
True False
True 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
  • index sucht wie find, aber wenn es kein solches Zeichen gibt, hält es das Programm mit ValueError an, statt -1 zurückzugeben.
  • isspace() sieht aus wie die Prüfung, die du brauchst, aber für leeren Text ist es False: 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)
Ausgabe
-1
False True
substring not found

Die 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)
Ausgabe
42-2.4

int() 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 = 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("Gesamt", total))
Ausgabe
Kaffee 2 x 2.40 = 4.80
Croissant 1 x 3.10 = 3.10
Mineralwasser 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 mit ValueError an: 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}")
Ausgabe
could not convert string to float: '2,40'
2.40

Probier 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.

Ü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.

Eingabe
Erika feuert nur untreue Fakire
Ausgabe
Palindrom
Eingabe
Erika feuert nur treue Fakire
Ausgabe
Kein Palindrom

2. 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.

Eingabe
3
Brot;1.20;2
Käse;9.80;1
Eier;0.35;10
Ausgabe
Brot 2 x 1.20 = 2.40
Käse 1 x 9.80 = 9.80
Eier 10 x 0.35 = 3.50
------------------------------------
Gesamt 15.70

3. 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.

Eingabe
Morgen um 9 ist eine Besprechung
Ausgabe
Besprechung eine ist 9 um Morgen

split() 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 join zusammen.
  • Versteckte E-Mail. Mach aus maria.becker@firma.de die Adresse m***********@firma.de: der erste Buchstabe, dann so viele Sternchen, wie es andere Zeichen vor dem @ gibt. Nimm find und *.
  • 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 mit is, nimm den Slice [0:8], trenne den zweiten beim Leerzeichen und gib die Teile aus, mach aus "100" eine Zahl und gib sie mit f"Wert: {...}" aus, und füge schließlich " und mehr" zu einer Liste mit dem ersten String hinzu und klebe sie mit join zusammen. 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

1. Warum ist report += line in einer Schleife mit 50.000 Durchläufen auf der obersten Ebene der Datei langsam?
2. Was ergibt "Bauer" > "Albrecht"?
3. a = ["12"]; b = a; b.append("3"). Was ergibt "".join(a)?
4. parts ist eine Liste von Strings. Wie machst du daraus einen einzigen String?
5. Was ist der Unterschied zwischen f"{total:6.2f}" und "{:6.2f}".format(total)?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·