Gibt es einen Grund, zwei Zeichenfolgen nicht mit '+' zu verketten?

123

Ein häufiges Antimuster in Python besteht darin, eine Folge von Zeichenfolgen +in einer Schleife zu verketten . Dies ist schlecht, da der Python-Interpreter für jede Iteration ein neues Zeichenfolgenobjekt erstellen muss und dies quadratische Zeit in Anspruch nimmt. (Neuere Versionen von CPython können dies anscheinend in einigen Fällen optimieren, andere Implementierungen jedoch nicht. Daher werden Programmierer davon abgehalten, sich darauf zu verlassen.) Dies ''.joinist der richtige Weg.

Allerdings habe ich gehört , dass es gesagt ( hier auf Stack - Überlauf einschließlich ) , dass Sie sollten nie, nie verwenden +für String - Verkettung, sondern immer verwenden ''.joinoder eine Format - String. Ich verstehe nicht, warum dies der Fall ist, wenn Sie nur zwei Zeichenfolgen verketten. Wenn mein Verständnis korrekt ist, sollte es keine quadratische Zeit dauern, und ich denke, es a + bist sauberer und lesbarer als entweder ''.join((a, b))oder '%s%s' % (a, b).

Ist es eine gute Praxis, +zwei Zeichenfolgen zu verketten? Oder gibt es ein Problem, das mir nicht bekannt ist?

Taymon
quelle
Es ist ordentlicher und Sie haben mehr Kontrolle, um keine Verkettung durchzuführen. ABER es ist etwas langsamer, String Bashing Kompromiss: P
Jakob Bowyer
Wollen Sie damit sagen, dass +es schneller oder langsamer ist? Und warum?
Taymon
1
+ ist schneller, In [2]: %timeit "a"*80 + "b"*80 1000000 loops, best of 3: 356 ns per loop In [3]: %timeit "%s%s" % ("a"*80, "b"*80) 1000000 loops, best of 3: 907 ns per loop
Jakob Bowyer
4
In [3]: %timeit "%s%s" % (a, b) 1000000 loops, best of 3: 590 ns per loop In [4]: %timeit a + b 10000000 loops, best of 3: 147 ns per loop
Jakob Bowyer
1
@JakobBowyer und andere: Das Argument "String-Verkettung ist schlecht" hat fast nichts mit Geschwindigkeit zu tun, sondern nutzt die automatische Typkonvertierung mit __str__. Siehe meine Antwort für Beispiele.
Izkata

Antworten:

119

Es ist nichts Falsches daran, zwei Zeichenfolgen mit zu verketten +. In der Tat ist es leichter zu lesen als ''.join([a, b]).

Sie haben jedoch Recht, dass das Verketten von mehr als 2 Zeichenfolgen mit +eine O (n ^ 2) -Operation ist (im Vergleich zu O (n) für join) und somit ineffizient wird. Dies hat jedoch nichts mit der Verwendung einer Schleife zu tun. Gerade a + b + c + ...ist O (n ^ 2), der Grund dafür ist, dass jede Verkettung eine neue Zeichenfolge erzeugt.

CPython2.4 und höher versuchen dies zu mildern, es ist jedoch weiterhin ratsam, es zu verwenden, joinwenn mehr als 2 Zeichenfolgen verkettet werden.

ggozad
quelle
5
@Mutant: Nimmt .joineine iterable, also beide .join([a,b])und .join((a,b))sind gültig.
Findel
1
Interessante Timings weisen auf Verwendung +oder +=in der akzeptierten Antwort (ab 2013) bei stackoverflow.com/a/12171382/378826 (von Lennart Regebro) auch für CPython 2.3+ und nur wählt den „append / join“ -Muster , wenn dies deutlicher macht die Idee für die vorliegende Problemlösung.
Dilettant
49

Der Plus-Operator ist eine perfekte Lösung, um zwei Python-Zeichenfolgen zu verketten . Wenn Sie jedoch immer mehr als zwei Zeichenfolgen hinzufügen (n> 25), möchten Sie möglicherweise etwas anderes denken.

''.join([a, b, c]) Trick ist eine Leistungsoptimierung.

Mikko Ohtamaa
quelle
2
Wäre ein Tupel nicht besser als eine Liste?
ThiefMaster
7
Tupel wäre schneller - der Code war nur ein Beispiel :) Normalerweise sind lange Eingaben mit mehreren Zeichenfolgen dynamisch.
Mikko Ohtamaa
5
@martineau Ich denke, er meint, append()Strings dynamisch zu generieren und zu einer Liste hinzuzufügen .
Peter C
5
Hier muss man sagen: Tupel ist normalerweise eine langsamere Struktur, besonders wenn es wächst. Mit list können Sie list.extend (list_of_items) und list.append (item) verwenden, die viel schneller sind, wenn Sie Dinge dynamisch verketten.
Antti Haapala
6
+1 für n > 25. Menschen brauchen Referenzpunkte, um irgendwo anzufangen.
n611x007
8

Die Annahme, dass man niemals + für die Verkettung von Zeichenfolgen verwenden sollte, sondern immer '' .join verwenden kann, kann ein Mythos sein. Es ist wahr, dass durch die Verwendung +unnötige temporäre Kopien eines unveränderlichen Zeichenfolgenobjekts erstellt werden, aber die andere nicht oft zitierte Tatsache ist, dass das Aufrufen joineiner Schleife im Allgemeinen den Overhead von erhöhen würde function call. Nehmen wir Ihr Beispiel.

Erstellen Sie zwei Listen, eine aus der verknüpften SO-Frage und eine größere fabrizierte

>>> myl1 = ['A','B','C','D','E','F']
>>> myl2=[chr(random.randint(65,90)) for i in range(0,10000)]

Lassen Sie uns zwei Funktionen erstellen UseJoinund UsePlusdie jeweiligen joinund +Funktionen verwenden.

>>> def UsePlus():
    return [myl[i] + myl[i + 1] for i in range(0,len(myl), 2)]

>>> def UseJoin():
    [''.join((myl[i],myl[i + 1])) for i in range(0,len(myl), 2)]

Lassen Sie uns mit der ersten Liste timeit ausführen

>>> myl=myl1
>>> t1=timeit.Timer("UsePlus()","from __main__ import UsePlus")
>>> t2=timeit.Timer("UseJoin()","from __main__ import UseJoin")
>>> print "%.2f usec/pass" % (1000000 * t1.timeit(number=100000)/100000)
2.48 usec/pass
>>> print "%.2f usec/pass" % (1000000 * t2.timeit(number=100000)/100000)
2.61 usec/pass
>>> 

Sie haben fast die gleiche Laufzeit.

Verwenden wir cProfile

>>> myl=myl2
>>> cProfile.run("UsePlus()")
         5 function calls in 0.001 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.001    0.001    0.001    0.001 <pyshell#1376>:1(UsePlus)
        1    0.000    0.000    0.001    0.001 <string>:1(<module>)
        1    0.000    0.000    0.000    0.000 {len}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        1    0.000    0.000    0.000    0.000 {range}


>>> cProfile.run("UseJoin()")
         5005 function calls in 0.029 CPU seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.015    0.015    0.029    0.029 <pyshell#1388>:1(UseJoin)
        1    0.000    0.000    0.029    0.029 <string>:1(<module>)
        1    0.000    0.000    0.000    0.000 {len}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
     5000    0.014    0.000    0.014    0.000 {method 'join' of 'str' objects}
        1    0.000    0.000    0.000    0.000 {range}

Und es sieht so aus, als würde die Verwendung von Join zu unnötigen Funktionsaufrufen führen, die den Overhead erhöhen könnten.

Kommen wir nun auf die Frage zurück. Sollte man in allen Fällen von der Verwendung von +over joinabraten?

Ich glaube nein, Dinge sollten berücksichtigt werden

  1. Länge der fraglichen Zeichenfolge
  2. Anzahl der Verkettungsvorgänge.

Und natürlich ist eine vorzeitige Optimierung in einer Entwicklung böse.

Abhijit
quelle
7
Die Idee wäre natürlich, nicht joininnerhalb der Schleife selbst zu verwenden, sondern die Schleife würde eine Sequenz erzeugen, die zum Verbinden übergeben würde.
Jsbueno
7

Bei der Arbeit mit mehreren Personen ist es manchmal schwierig, genau zu wissen, was passiert. Die Verwendung einer Formatzeichenfolge anstelle einer Verkettung kann eine bestimmte Störung vermeiden, die uns unzählige Male passiert ist:

Angenommen, eine Funktion erfordert ein Argument, und Sie schreiben es in der Erwartung, eine Zeichenfolge zu erhalten:

In [1]: def foo(zeta):
   ...:     print 'bar: ' + zeta

In [2]: foo('bang')
bar: bang

Daher kann diese Funktion im gesamten Code ziemlich häufig verwendet werden. Ihre Mitarbeiter wissen möglicherweise genau, was sie tun, sind jedoch nicht unbedingt auf dem neuesten Stand der Interna und wissen möglicherweise nicht, dass die Funktion eine Zeichenfolge erwartet. Und so können sie am Ende damit enden:

In [3]: foo(23)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)

/home/izkata/<ipython console> in <module>()

/home/izkata/<ipython console> in foo(zeta)

TypeError: cannot concatenate 'str' and 'int' objects

Es wäre kein Problem, wenn Sie nur eine Formatzeichenfolge verwenden würden:

In [1]: def foo(zeta):
   ...:     print 'bar: %s' % zeta
   ...:     
   ...:     

In [2]: foo('bang')
bar: bang

In [3]: foo(23)
bar: 23

Gleiches gilt für alle Arten von Objekten, die definieren __str__und die ebenfalls übergeben werden können:

In [1]: from datetime import date

In [2]: zeta = date(2012, 4, 15)

In [3]: print 'bar: ' + zeta
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)

/home/izkata/<ipython console> in <module>()

TypeError: cannot concatenate 'str' and 'datetime.date' objects

In [4]: print 'bar: %s' % zeta
bar: 2012-04-15

Also ja: Wenn Sie eine Formatzeichenfolge verwenden können, tun Sie dies und nutzen Sie das, was Python zu bieten hat.

Izkata
quelle
1
+1 für eine gut begründete abweichende Meinung. Ich denke immer noch, dass ich es favorisiere +.
Taymon
1
Warum definieren Sie die foo-Methode nicht einfach wie folgt: print 'bar:' + str (zeta)?
EngineerWithJava54321
@ EngineerWithJava54321 Zum Beispiel zeta = u"a\xac\u1234\u20ac\U00008000"- Sie müssten also verwenden, print 'bar: ' + unicode(zeta)um sicherzustellen, dass kein Fehler auftritt. %smacht es richtig, ohne darüber nachdenken zu müssen, und ist viel kürzer
Izkata
@ EngineerWithJava54321 Weitere Beispiele weniger relevant hier sind, aber zum Beispiel "bar: %s"könnte übersetzt werden "zrb: %s br"in einer anderen Sprache. Die %sVersion wird nur funktionieren, aber die String-Concat-Version würde ein Chaos werden, um alle Fälle zu behandeln, und Ihre Übersetzer hätten jetzt zwei separate Übersetzungen zu
erledigen
Wenn sie nicht wissen, was die Implementierung von foo ist, werden sie mit jedem auf diesen Fehler stoßen def.
Insidesin
3

Ich habe einen kurzen Test gemacht:

import sys

str = e = "a xxxxxxxxxx very xxxxxxxxxx long xxxxxxxxxx string xxxxxxxxxx\n"

for i in range(int(sys.argv[1])):
    str = str + e

und zeitlich festgelegt:

mslade@mickpc:/binks/micks/ruby/tests$ time python /binks/micks/junk/strings.py  8000000
8000000 times

real    0m2.165s
user    0m1.620s
sys     0m0.540s
mslade@mickpc:/binks/micks/ruby/tests$ time python /binks/micks/junk/strings.py  16000000
16000000 times

real    0m4.360s
user    0m3.480s
sys     0m0.870s

Es gibt anscheinend eine Optimierung für den a = a + bFall. Es zeigt keine O (n ^ 2) -Zeit, wie man vermuten könnte.

Zumindest in Bezug auf die Leistung ist die Verwendung +in Ordnung.

Michael Slade
quelle
3
Sie können hier mit dem Fall "Join" vergleichen. Und es geht um andere Python-Implementierungen wie Pypy, Jython, Ironpython usw.
jsbueno
3

Laut Python-Dokumenten erhalten Sie mit str.join () eine konsistente Leistung für verschiedene Implementierungen von Python. Obwohl CPython das quadratische Verhalten von s = s + t optimiert, ist dies bei anderen Python-Implementierungen möglicherweise nicht der Fall.

Details zur CPython-Implementierung : Wenn s und t beide Zeichenfolgen sind, können einige Python-Implementierungen wie CPython normalerweise eine direkte Optimierung für Zuweisungen der Form s = s + t oder s + = t durchführen. Wenn zutreffend, macht diese Optimierung die quadratische Laufzeit viel weniger wahrscheinlich. Diese Optimierung ist sowohl version- als auch implementierungsabhängig. Für leistungsempfindlichen Code wird vorzugsweise die Methode str.join () verwendet, die eine konsistente lineare Verkettungsleistung über Versionen und Implementierungen hinweg gewährleistet.

Sequenztypen in Python-Dokumenten (siehe Fußnote [6])

Herzog
quelle
2

Ich benutze das Folgende mit Python 3.8

string4 = f'{string1}{string2}{string3}'
Lucas Vazquez
quelle
0

'' .join ([a, b]) ist eine bessere Lösung als + .

Weil Code so geschrieben werden sollte, dass andere Implementierungen von Python (PyPy, Jython, IronPython, Cython, Psyco usw.) nicht benachteiligt werden.

Form a + = b oder a = a + b ist selbst in CPython fragil und in Implementierungen ohne Refcounting überhaupt nicht vorhanden (Referenzzählung ist eine Technik zum Speichern der Anzahl von Referenzen, Zeigern oder Handles auf a Ressource wie ein Objekt, ein Speicherblock, Speicherplatz oder eine andere Ressource )

https://www.python.org/dev/peps/pep-0008/#programming-recommendations

muhammad ali e
quelle
1
a += bfunktioniert in allen Implementierungen von Python, es ist nur so, dass es bei einigen von ihnen quadratische Zeit dauert, wenn es in einer Schleife ausgeführt wird ; Die Frage betraf die Verkettung von Zeichenfolgen außerhalb einer Schleife.
Taymon