Was ist die effizienteste Methode zur Verkettung von Zeichenfolgen in Python?


148

Gibt es in Python eine effiziente Methode zur Verkettung von Massenzeichenfolgen (wie StringBuilder in C # oder StringBuffer in Java)? Ich habe hier folgende Methoden gefunden :

  • Einfache Verkettung mit +
  • Verwenden der Zeichenfolgenliste und join-methode
  • Verwendung UserStringvom MutableStringModul
  • Zeichenarray und das arrayModul verwenden
  • Verwendung cStringIOvom StringIOModul

Aber was verwenden oder schlagen Experten vor und warum?

[ Eine verwandte Frage hier ]



Um bekannte Fragmente zu einem zu verketten, verfügt Python 3.6 über f''Formatzeichenfolgen, die schneller sind als alle Alternativen in früheren Python-Versionen.
Antti Haapala

Antworten:


127

Das könnte Sie interessieren: Eine Optimierungsanekdote von Guido. Obwohl es sich auch daran zu erinnern lohnt, dass dies ein alter Artikel ist und vor der Existenz von Dingen wie ''.join(obwohl ich denke, dass string.joinfieldses mehr oder weniger dasselbe ist)

Aufgrund dessen ist das arrayModul möglicherweise am schnellsten, wenn Sie Ihr Problem darin einschieben können. Ist ''.joinaber wahrscheinlich schnell genug und hat den Vorteil, dass es idiomatisch ist und somit für andere Python-Programmierer leichter zu verstehen ist.

Schließlich die goldene Regel der Optimierung: Optimieren Sie nicht, es sei denn, Sie wissen, dass Sie es müssen, und messen Sie, anstatt zu raten.

Mit dem timeitModul können Sie verschiedene Methoden messen . Das kann Ihnen sagen , welches am schnellsten ist, anstatt dass zufällige Fremde im Internet Vermutungen anstellen.


1
Möchten Sie den Punkt hinzufügen, wann optimiert werden muss: Stellen Sie sicher, dass Sie gegen die schlimmsten Fälle testen. Zum Beispiel kann ich mein Beispiel erhöhen, sodass mein aktueller Code von 0,17 Sekunden auf 170 Sekunden läuft. Nun, ich möchte bei größeren Stichproben testen, da es dort weniger Variationen gibt.
Flipper

2
"Optimieren Sie nicht, bis Sie wissen, dass Sie müssen." Es sei denn, Sie verwenden nur eine nominell andere Sprache und können die Überarbeitung Ihres Codes mit geringem Aufwand vermeiden.
Jeremyjjbrown

1
Ein Ort, von dem Sie wissen, dass Sie ihn brauchen, ist das Interview (was immer eine gute Zeit ist, um Ihr tiefes Verständnis aufzufrischen). Leider habe ich keinen modernen Artikel darüber gefunden. (1) Ist Java / C # String 2017 noch so schlecht? (2) Wie wäre es mit C ++? (3) Erzählen Sie jetzt über die neuesten und besten Entwicklungen in Python und konzentrieren Sie sich auf Fälle, in denen wir Millionen von Verkettungen durchführen müssen. Können wir darauf vertrauen, dass Join in linearer Zeit funktioniert?
user1854182

Wofür bedeutet "schnell genug" .join()? Die Hauptfrage ist, ob a) eine Kopie der Zeichenfolge zur Verkettung erstellt wird (ähnlich wie s = s + 'abc'), für die O (n) Laufzeit erforderlich ist, oder b) einfach an die vorhandene Zeichenfolge angehängt wird, ohne eine Kopie zu erstellen, für die O (1) erforderlich ist. ?
CGFoX

64

''.join(sequenceofstrings) funktioniert normalerweise am besten - am einfachsten und am schnellsten.


3
@mshsayem, in Python kann eine Sequenz ein beliebiges aufzählbares Objekt sein, sogar eine Funktion.
Nick Dandoulakis

2
Ich liebe die ''.join(sequence)Redewendung absolut . Es ist besonders nützlich, durch Kommas getrennte Listen zu erstellen: ', '.join([1, 2, 3])Gibt die Zeichenfolge an '1, 2, 3'.
Andrew Keeton

7
@mshsayem: "".join(chr(x) for x in xrange(65,91))--- In diesem Fall ist das zu verbindende Argument ein Iterator, der durch einen Generatorausdruck erstellt wird. Es gibt keine temporäre Liste, die erstellt wird.
Balpha

2
@balpha: und dennoch ist die Generatorversion langsamer als die Version zum Listenverständnis: C: \ temp> python -mtimeit "'' .join (chr (x) für x in xrange (65,91))" 100000 Schleifen, am besten von 3: 9,71 usec pro Schleife C: \ temp> python -mtimeit "'' .join ([chr (x) für x in xrange (65,91)])" 100000 Schleifen, am besten 3: 7,1 usec pro Schleife
hughdbrown

1
@hughdbrown, ja, wenn Sie freien Speicherplatz haben, kann der Wazoo-Listcomp (typischer Timeit-Fall) besser optimiert werden als Genexp, oft um 20-30%. Wenn die engen Dinge des Gedächtnisses anders sind - aber zeitlich schwer zu reproduzieren! -)
Alex Martelli

58

Python 3.6 hat das Spiel für die String-Verkettung bekannter Komponenten mit Literal String Interpolation geändert .

Angesichts des Testfalls aus der Antwort von mkoistinen mit Strings

domain = 'some_really_long_example.com'
lang = 'en'
path = 'some/really/long/path/'

Die Anwärter sind

  • f'http://{domain}/{lang}/{path}'- 0,151 us

  • 'http://%s/%s/%s' % (domain, lang, path) - 0,321 us

  • 'http://' + domain + '/' + lang + '/' + path - 0,356 us

  • ''.join(('http://', domain, '/', lang, '/', path))- 0,249 µs ( Beachten Sie , dass das Erstellen eines Tupels mit konstanter Länge etwas schneller ist als das Erstellen einer Liste mit konstanter Länge).

Somit ist derzeit der kürzeste und schönste mögliche Code auch am schnellsten.

In Alpha-Versionen von Python 3.6 war die Implementierung von f''Zeichenfolgen so langsam wie möglich - tatsächlich entspricht der generierte Bytecode dem ''.join()Fall mit unnötigen Aufrufen, zu str.__format__denen ohne Argumente nur selfunverändert zurückgegeben werden würde. Diese Ineffizienzen wurden vor 3.6 final behoben.

Die Geschwindigkeit kann mit der schnellsten Methode für Python 2 verglichen werden, nämlich der +Verkettung auf meinem Computer. und das dauert 0,203 µs mit 8-Bit-Strings und 0,259 µs, wenn alle Strings Unicode sind.


38

Es hängt davon ab, was Sie tun.

Nach Python 2.5 ist die Verkettung von Zeichenfolgen mit dem Operator + ziemlich schnell. Wenn Sie nur einige Werte verketten, funktioniert die Verwendung des Operators + am besten:

>>> x = timeit.Timer(stmt="'a' + 'b'")
>>> x.timeit()
0.039999961853027344

>>> x = timeit.Timer(stmt="''.join(['a', 'b'])")
>>> x.timeit()
0.76200008392333984

Wenn Sie jedoch eine Zeichenfolge in einer Schleife zusammenstellen, sollten Sie die Listenverbindungsmethode verwenden:

>>> join_stmt = """
... joined_str = ''
... for i in xrange(100000):
...   joined_str += str(i)
... """
>>> x = timeit.Timer(join_stmt)
>>> x.timeit(100)
13.278000116348267

>>> list_stmt = """
... str_list = []
... for i in xrange(100000):
...   str_list.append(str(i))
... ''.join(str_list)
... """
>>> x = timeit.Timer(list_stmt)
>>> x.timeit(100)
12.401000022888184

... aber beachten Sie, dass Sie eine relativ hohe Anzahl von Zeichenfolgen zusammenstellen müssen, bevor der Unterschied spürbar wird.


2
1) Bei Ihrer ersten Messung ist es wahrscheinlich die Listenkonstruktion, die die Zeit in Anspruch nimmt. Versuchen Sie es mit einem Tupel. 2) CPython schneidet gleichmäßig gut ab, andere Python-Implementierungen
schneiden jedoch

22

Optimieren Sie gemäß John Fouhys Antwort nicht, es sei denn, Sie müssen, aber wenn Sie hier sind und diese Frage stellen, kann dies genau daran liegen, dass Sie müssen . In meinem Fall musste ich einige URLs aus Zeichenfolgenvariablen zusammenstellen ... schnell. Mir ist aufgefallen, dass (bisher) niemand über die String-Format-Methode nachdenkt, also dachte ich, ich würde das versuchen, und hauptsächlich aus mildem Interesse dachte ich, ich würde den String-Interpolationsoperator für eine gute Messung hineinwerfen. Um ehrlich zu sein, hätte ich nicht gedacht, dass sich beides zu einer direkten '+' - Operation oder einer '' .join () zusammenfügen würde. Aber rate mal was? Auf meinem Python 2.7.5-System regiert der String-Interpolationsoperator sie alle und string.format () ist der schlechteste Performer:

# concatenate_test.py

from __future__ import print_function
import timeit

domain = 'some_really_long_example.com'
lang = 'en'
path = 'some/really/long/path/'
iterations = 1000000

def meth_plus():
    '''Using + operator'''
    return 'http://' + domain + '/' + lang + '/' + path

def meth_join():
    '''Using ''.join()'''
    return ''.join(['http://', domain, '/', lang, '/', path])

def meth_form():
    '''Using string.format'''
    return 'http://{0}/{1}/{2}'.format(domain, lang, path)

def meth_intp():
    '''Using string interpolation'''
    return 'http://%s/%s/%s' % (domain, lang, path)

plus = timeit.Timer(stmt="meth_plus()", setup="from __main__ import meth_plus")
join = timeit.Timer(stmt="meth_join()", setup="from __main__ import meth_join")
form = timeit.Timer(stmt="meth_form()", setup="from __main__ import meth_form")
intp = timeit.Timer(stmt="meth_intp()", setup="from __main__ import meth_intp")

plus.val = plus.timeit(iterations)
join.val = join.timeit(iterations)
form.val = form.timeit(iterations)
intp.val = intp.timeit(iterations)

min_val = min([plus.val, join.val, form.val, intp.val])

print('plus %0.12f (%0.2f%% as fast)' % (plus.val, (100 * min_val / plus.val), ))
print('join %0.12f (%0.2f%% as fast)' % (join.val, (100 * min_val / join.val), ))
print('form %0.12f (%0.2f%% as fast)' % (form.val, (100 * min_val / form.val), ))
print('intp %0.12f (%0.2f%% as fast)' % (intp.val, (100 * min_val / intp.val), ))

Die Ergebnisse:

# python2.7 concatenate_test.py
plus 0.360787868500 (90.81% as fast)
join 0.452811956406 (72.36% as fast)
form 0.502608060837 (65.19% as fast)
intp 0.327636957169 (100.00% as fast)

Wenn ich eine kürzere Domäne und einen kürzeren Pfad verwende, gewinnt die Interpolation immer noch. Der Unterschied ist jedoch bei längeren Saiten stärker ausgeprägt.

Nachdem ich nun ein schönes Testskript hatte, habe ich auch unter Python 2.6, 3.3 und 3.4 getestet. Hier sind die Ergebnisse. In Python 2.6 ist der Plus-Operator der schnellste! Bei Python 3 gewinnt Join. Hinweis: Diese Tests sind auf meinem System sehr wiederholbar. Daher ist 'plus' in 2.6 immer schneller, 'intp' in 2.7 immer schneller und 'join' in Python 3.x immer schneller.

# python2.6 concatenate_test.py
plus 0.338213920593 (100.00% as fast)
join 0.427221059799 (79.17% as fast)
form 0.515371084213 (65.63% as fast)
intp 0.378169059753 (89.43% as fast)

# python3.3 concatenate_test.py
plus 0.409130576998 (89.20% as fast)
join 0.364938726001 (100.00% as fast)
form 0.621366866995 (58.73% as fast)
intp 0.419064424001 (87.08% as fast)

# python3.4 concatenate_test.py
plus 0.481188605998 (85.14% as fast)
join 0.409673971997 (100.00% as fast)
form 0.652010936996 (62.83% as fast)
intp 0.460400978001 (88.98% as fast)

# python3.5 concatenate_test.py
plus 0.417167026084 (93.47% as fast)
join 0.389929617057 (100.00% as fast)
form 0.595661019906 (65.46% as fast)
intp 0.404455224983 (96.41% as fast)

Lektion gelernt:

  • Manchmal sind meine Annahmen absolut falsch.
  • Test gegen die Systemumgebung Sie werden in der Produktion laufen.
  • String-Interpolation ist noch nicht tot!

tl; dr:

  • Wenn Sie 2.6 verwenden, verwenden Sie den Operator +.
  • Wenn Sie 2.7 verwenden, verwenden Sie den Operator '%'.
  • Wenn Sie 3.x verwenden, verwenden Sie '' .join ().

2
Hinweis: Die Interpolation von Literal-Strings ist ab 3.6 noch schneller:f'http://{domain}/{lang}/{path}'
TemporalWolf

1
Außerdem .format()hat drei Formen, um von schnell zu langsam: "{}".format(x), "{0}".format(x),"{x}".format(x=x)
TemporalWolf

Die eigentliche Lektion: Wenn Ihre Problemdomäne klein ist, z. B. kurze Zeichenfolgen, spielt die Methode meistens keine Rolle. Und selbst wenn es darauf ankommt, z. B. wenn Sie wirklich eine Million Saiten bauen, ist der Overhead oft wichtiger. Es ist ein typisches Symptom für die Sorge um das falsche Problem. Nur wenn der Overhead nicht signifikant ist, z. B. wenn das gesamte Buch als Zeichenfolge aufgebaut wird, spielt der Methodenunterschied eine Rolle.
Hui Zhou

7

Dies hängt ziemlich stark von der relativen Größe der neuen Zeichenfolge nach jeder neuen Verkettung ab. Mit dem +Operator wird für jede Verkettung eine neue Zeichenfolge erstellt. Wenn die Zwischenzeichenfolgen relativ lang sind, +wird die zunehmend langsamer, da die neue Zwischenzeichenfolge gespeichert wird.

Betrachten Sie diesen Fall:

from time import time
stri=''
a='aagsdfghfhdyjddtyjdhmfghmfgsdgsdfgsdfsdfsdfsdfsdfsdfddsksarigqeirnvgsdfsdgfsdfgfg'
l=[]
#case 1
t=time()
for i in range(1000):
    stri=stri+a+repr(i)
print time()-t

#case 2
t=time()
for i in xrange(1000):
    l.append(a+repr(i))
z=''.join(l)
print time()-t

#case 3
t=time()
for i in range(1000):
    stri=stri+repr(i)
print time()-t

#case 4
t=time()
for i in xrange(1000):
    l.append(repr(i))
z=''.join(l)
print time()-t

Ergebnisse

1 0,00493192672729

2 0,000509023666382

3 0,00042200088501

4 0,000482797622681

Im Fall von 1 & 2 fügen wir eine große Zeichenfolge hinzu, und join () führt etwa zehnmal schneller aus. In den Fällen 3 und 4 fügen wir eine kleine Zeichenfolge hinzu, und '+' wird etwas schneller ausgeführt


3

Ich geriet in eine Situation, in der ich eine anhängbare Zeichenfolge unbekannter Größe benötigen würde. Dies sind die Benchmark-Ergebnisse (Python 2.7.3):

$ python -m timeit -s 's=""' 's+="a"'
10000000 loops, best of 3: 0.176 usec per loop
$ python -m timeit -s 's=[]' 's.append("a")'
10000000 loops, best of 3: 0.196 usec per loop
$ python -m timeit -s 's=""' 's="".join((s,"a"))'
100000 loops, best of 3: 16.9 usec per loop
$ python -m timeit -s 's=""' 's="%s%s"%(s,"a")'
100000 loops, best of 3: 19.4 usec per loop

Dies scheint zu zeigen, dass '+ =' am schnellsten ist. Die Ergebnisse des Skymind-Links sind etwas veraltet.

(Mir ist klar, dass das zweite Beispiel nicht vollständig ist und die endgültige Liste zusammengefügt werden muss. Dies zeigt jedoch, dass das einfache Erstellen der Liste länger dauert als die Zeichenfolge concat.)


Ich bekomme mal 1 Sekunde für den 3. und 4. Test. Warum bekommst du so hohe Zeiten? pastebin.com/qabNMCHS
bad_keypoints

@ronnieaka: Er bekommt für alle Tests Zeiten unter 1 Sekunde. Er bekommt> 1 µs für den 3. und 4., was Sie nicht getan haben. Ich bekomme auch langsamere Zeiten bei diesen Tests (unter Python 2.7.5, Linux). Könnte CPU, Version, Build-Flags sein, wer weiß.
Thanatos

Diese Benchmark-Ergebnisse sind nutzlos. Insbesondere im ersten Fall, in dem keine Zeichenfolgenverkettung durchgeführt wird, wird nur der zweite Zeichenfolgenwert intakt zurückgegeben.
Antti Haapala

3

Ein Jahr später testen wir die Antwort von mkoistinen mit Python 3.4.3:

  • plus 0,963564149000 (95,83% so schnell)
  • Join 0.923408469000 (100,00% so schnell)
  • Formular 1.501130934000 (61,51% so schnell)
  • intp 1.019677452000 (90,56% so schnell)

Nichts hat sich geändert. Join ist immer noch die schnellste Methode. Da intp in Bezug auf die Lesbarkeit wohl die beste Wahl ist, möchten Sie möglicherweise dennoch intp verwenden.


1
Vielleicht könnte es eine Ergänzung zu mkoistinen Antwort sein, da es ein bisschen zu kurz für eine vollständige Antwort ist (oder zumindest den Code hinzufügen, den Sie verwenden).
Trilarion

1

Inspiriert von den Benchmarks von @ JasonBaker, ist hier eine einfache, die 10 "abcdefghijklmnopqrstuvxyz"Zeichenfolgen vergleicht und zeigt, dass dies .join()schneller ist. selbst mit dieser winzigen Zunahme von Variablen:

Verkettung

>>> x = timeit.Timer(stmt='"abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz" + "abcdefghijklmnopqrstuvxyz"')
>>> x.timeit()
0.9828147209324385

Beitreten

>>> x = timeit.Timer(stmt='"".join(["abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz"])')
>>> x.timeit()
0.6114138159765048

Schauen Sie sich die akzeptierte Antwort (lange nach unten scrollen) dieser Frage an: stackoverflow.com/questions/1349311/…
mshsayem

1

Für einen kleinen Satz von kurzen Strings (dh 2 oder 3 Saiten von nicht mehr als ein paar Zeichen), und ist immer noch viel schneller. Verwenden von mkoistinens wundervollem Skript in Python 2 und 3:

plus 2.679107467004 (100.00% as fast)
join 3.653773699996 (73.32% as fast)
form 6.594011374000 (40.63% as fast)
intp 4.568015249999 (58.65% as fast)

Wenn Ihr Code also eine große Anzahl separater kleiner Verkettungen ausführt, ist Plus der bevorzugte Weg, wenn die Geschwindigkeit entscheidend ist.


1

Wahrscheinlich ist "Neue F-Strings in Python 3.6" die effizienteste Art, Strings zu verketten.

Mit% s

>>> timeit.timeit("""name = "Some"
... age = 100
... '%s is %s.' % (name, age)""", number = 10000)
0.0029734770068898797

Verwenden von .format

>>> timeit.timeit("""name = "Some"
... age = 100
... '{} is {}.'.format(name, age)""", number = 10000)
0.004015227983472869

Mit f

>>> timeit.timeit("""name = "Some"
... age = 100
... f'{name} is {age}.'""", number = 10000)
0.0019175919878762215

Quelle: https://realpython.com/python-f-strings/

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.