23.2 Strings 

Bei der Arbeit mit Strings sollten Sie immer im Hinterkopf behalten, dass Strings immutable sind. Das bedeutet, dass bei jeder Veränderung eines Strings eine neue Instanz des Datentyps str erzeugt werden muss. Dieses Verhalten lässt sich nicht ändern, was sich aber erreichen lässt, ist, dass möglichst wenig neue Instanzen erzeugt werden müssen. Betrachten Sie dazu folgendes Beispiel, in dem wir alle in einer Liste enthaltenen Strings durch eine Funktion f schicken möchten. Die Rückgabewerte all dieser Funktionsaufrufe sollen zu einem einzigen langen String zusammengefasst werden. Ganz blauäugig würden wir das Problem erst einmal folgendermaßen lösen:
def f(s): return s.upper()
alle_strings = ["Hallo Welt"]*200000 string = "" for s in alle_strings: string += f(s)
Zunächst wird die Funktion f definiert, die den übergebenen String s in Großbuchstaben konvertiert und zurückgibt. Beachten Sie, dass diese Funktion zunächst nicht zur Debatte stehen soll, sondern dass wir uns auf den nun folgenden Code konzentrieren möchten. In diesem wird über eine Liste von 200.000 Strings iteriert. In jedem Iterationsschritt wird die Funktion f für den aktuellen String aufgerufen und das Ergebnis an den String string gehängt. Bei jedem Anhängen eines neuen Strings wird eine neue Instanz des Datentyps str erzeugt, was laufzeittechnisch gesehen glatter Wahnsinn ist.
Das ständige Erzeugen neuer str-Instanzen kann unterbunden werden, indem die von f zurückgegebenen Strings zunächst in einem veränderlichen Datentyp, beispielsweise einer Liste, zwischengespeichert und erst nach Durchlauf der Schleife in einer einzelnen Operation zusammengefügt werden. Dazu kann die String-Methode join verwendet werden:
alle_strings = ["Hallo Welt"]*200000 lst = [None]*len(alle_strings) for i in xrange(len(alle_strings)): lst[i] = f(alle_strings[i]) string = "".join(lst)
In diesem Beispiel wird zunächst eine Liste erzeugt, die ebenso viele Einträge umfasst wie die Liste alle_strings. In jedem Element der neuen Liste lst wird None referenziert. Das ist besonders günstig, da von None immer nur eine Instanz existiert und somit alle Elemente der Liste auf dieselbe Instanz verweisen. Damit wird keine Laufzeit zur Erzeugung überflüssiger Instanzen verschwendet. Im zweiten Schritt werden in einer for-Schleife alle ganzzahligen Indizes zwischen 0 und 199.999 durchlaufen und das jeweilige Element der Liste alle_strings durch die Funktion f geschickt und in die neue Liste lst eingetragen. Das Eintragen in die neue Liste ist besonders günstig, da nur Elemente ersetzt werden müssen, sich die Länge der Liste also nicht verändert. In der letzten Zeile des Beispiels werden schlussendlich alle Elemente der neu erzeugten Liste lst durch Aufruf der String-Methode join zu einem großen String zusammengefasst. Wenn man die Laufzeit der beiden Beispiele vergleicht, stellt man fest, dass die Laufzeit des unteren Beispiels um 20 % geringer ist als die des oberen.
Beachten Sie, dass sich dieser Optimierungsansatz nur bei wirklich großen Listen auszahlt und bei kleinen sogar eher kontraproduktiv ist. Dennoch gilt die Maxime, mehrere Strings nicht mit dem Operator + zu verbinden. So könnte der Code
string = a + b + c + d
mit den Strings a, b, c und d viel effizienter in dieser Form geschrieben werden:
string = "%s%s%s%s", (a, b, c, d)
da in diesem Fall nur eine statt drei neuer Instanzen für die jeweiligen Zwischenergebnisse der Addition erzeugt werden muss.