Welches ist die bevorzugte Methode, um einen String in Python zu verketten?

358

Da Pythons stringnicht geändert werden können, habe ich mich gefragt, wie ich einen String effizienter verketten kann.

Ich kann so schreiben:

s += stringfromelsewhere

oder so:

s = []
s.append(somestring)

later

s = ''.join(s)

Während ich diese Frage schrieb, fand ich einen guten Artikel über das Thema.

http://www.skymind.com/~ocrow/python_string/

Aber es ist in Python 2.x., also wäre die Frage, ob sich in Python 3 etwas geändert hat?

Max
quelle

Antworten:

433

Der beste Weg, eine Zeichenfolge an eine Zeichenfolgenvariable anzuhängen, ist die Verwendung von +oder +=. Dies liegt daran, dass es lesbar und schnell ist. Sie sind auch genauso schnell, welche Sie wählen, ist Geschmackssache, die letztere ist die häufigste. Hier sind Timings mit dem timeitModul:

a = a + b:
0.11338996887207031
a += b:
0.11040496826171875

Diejenigen, die empfehlen, Listen zu haben und an diese anzuhängen und diese dann zu verbinden, tun dies jedoch, da das Anhängen einer Zeichenfolge an eine Liste im Vergleich zum Erweitern einer Zeichenfolge vermutlich sehr schnell ist. Und dies kann in einigen Fällen zutreffen. Hier ist zum Beispiel eine Million Anhänge einer Zeichenfolge mit einem Zeichen, zuerst an eine Zeichenfolge, dann an eine Liste:

a += b:
0.10780501365661621
a.append(b):
0.1123361587524414

OK, es stellt sich heraus, dass das Anhängen auch dann noch schneller war, wenn die resultierende Zeichenfolge eine Million Zeichen lang ist.

Versuchen wir nun, eine tausend Zeichen lange Zeichenfolge hunderttausend Mal anzuhängen:

a += b:
0.41823482513427734
a.append(b):
0.010656118392944336

Die Endzeichenfolge ist daher ungefähr 100 MB lang. Das war ziemlich langsam, das Anhängen an eine Liste war viel schneller. Dass dieses Timing nicht das Finale beinhaltet a.join(). Wie lange würde das dauern?

a.join(a):
0.43739795684814453

Oups. Auch in diesem Fall ist das Anhängen / Verbinden langsamer.

Woher kommt diese Empfehlung? Python 2?

a += b:
0.165287017822
a.append(b):
0.0132720470428
a.join(a):
0.114929914474

Nun, append / join ist geringfügig es schneller , wenn Sie extrem lange Strings verwenden (was Sie sind in der Regel nicht, was würden Sie eine Zeichenfolge, die 100 MB im Speicher ist?)

Aber der wahre Drahtreifen ist Python 2.3. Wo ich dir nicht einmal die Timings zeigen werde, weil es so langsam ist, dass es noch nicht fertig ist. Diese Tests dauern plötzlich Minuten . Mit Ausnahme des Anhängens / Verbindens, das genauso schnell ist wie unter späteren Pythons.

Jep. Die Verkettung von Strings war in Python in der Steinzeit sehr langsam. Unter 2.4 ist dies jedoch nicht mehr der Fall (oder zumindest Python 2.4.7). Daher wurde die Empfehlung zur Verwendung von Anhängen / Verknüpfen 2008 veraltet, als Python 2.3 nicht mehr aktualisiert wurde und Sie es nicht mehr verwenden sollten. :-)

(Update: Es stellte sich heraus, dass ich die Tests sorgfältiger durchgeführt habe als die Verwendung von +und +=schneller für zwei Zeichenfolgen in Python 2.3. Die Empfehlung zur Verwendung ''.join()muss ein Missverständnis sein.)

Dies ist jedoch CPython. Andere Implementierungen können andere Bedenken haben. Und dies ist nur ein weiterer Grund, warum vorzeitige Optimierung die Wurzel allen Übels ist. Verwenden Sie keine Technik, die "schneller" sein soll, es sei denn, Sie messen sie zuerst.

Daher ist die "beste" Version für die Verkettung von Zeichenfolgen die Verwendung von + oder + = . Und wenn sich herausstellt, dass das für Sie langsam ist, was ziemlich unwahrscheinlich ist, dann tun Sie etwas anderes.

Warum verwende ich in meinem Code viel Anhängen / Verbinden? Weil es manchmal tatsächlich klarer ist. Besonders wenn alles, was Sie miteinander verketten sollten, durch Leerzeichen, Kommas oder Zeilenumbrüche getrennt werden sollte.

Lennart Regebro
quelle
10
Wenn Sie mehrere Zeichenfolgen haben (n> 10) "" .join (list_of_strings) ist immer noch schneller
Mikko Ohtamaa
11
Der Grund, warum + = schnell ist, ist, dass es in cpython einen Performance-Hack gibt, wenn der Refcount 1 ist - er fällt bei so ziemlich allen anderen Python-Implementierungen auseinander (mit Ausnahme eines ziemlich speziell konfigurierten Pypy-Builds)
Ronny
17
Warum wird das so positiv bewertet? Wie ist es besser, einen Algorithmus zu verwenden, der nur für eine bestimmte Implementierung effizient ist und im Wesentlichen einen fragilen Hack zur Behebung eines quadratischen Zeitalgorithmus darstellt? Auch Sie verstehen den Punkt "vorzeitige Optimierung ist die Wurzel allen Übels" völlig falsch. In diesem Zitat geht es um kleine Optimierungen. Dies geht von O (n ^ 2) nach O (n), was KEINE kleine Optimierung ist.
Wes
12
Hier ist das eigentliche Zitat: "Wir sollten kleine Wirkungsgrade vergessen, etwa 97% der Zeit: Vorzeitige Optimierung ist die Wurzel allen Übels. Dennoch sollten wir unsere Chancen in diesen kritischen 3% nicht verpassen. Ein guter Programmierer wird dies nicht tun." Wenn er durch solche Überlegungen zur Selbstzufriedenheit gebracht wird, sollte er sich den kritischen Code genau ansehen, aber erst, nachdem dieser Code identifiziert wurde "
Wes
2
Niemand sagt, dass a + b langsam ist. Es ist quadratisch, wenn Sie a = a + b mehr als einmal ausführen. a + b + c ist nicht langsam, ich wiederhole nicht langsam, da es jede Zeichenfolge nur einmal durchlaufen muss, während es die vorherigen Zeichenfolgen mit dem Ansatz a = a + b viele Male erneut durchlaufen muss (vorausgesetzt, es befindet sich in einer Schleife irgendeiner Art). Denken Sie daran, dass Zeichenfolgen unveränderlich sind.
Wes
52

Wenn Sie viele Werte verketten, dann keine. Das Anhängen einer Liste ist teuer. Sie können dafür StringIO verwenden. Vor allem, wenn Sie es über viele Operationen aufbauen.

from cStringIO import StringIO
# python3:  from io import StringIO

buf = StringIO()

buf.write('foo')
buf.write('foo')
buf.write('foo')

buf.getvalue()
# 'foofoofoo'

Wenn Sie bereits eine vollständige Liste von einem anderen Vorgang erhalten haben, verwenden Sie einfach die ''.join(aList)

Aus den Python-FAQ: Was ist der effizienteste Weg, um viele Zeichenfolgen miteinander zu verketten?

str- und bytes-Objekte sind unveränderlich, daher ist das Verketten vieler Zeichenfolgen ineffizient, da bei jeder Verkettung ein neues Objekt erstellt wird. Im allgemeinen Fall sind die Gesamtlaufzeitkosten quadratisch in der Gesamtlänge der Zeichenfolge.

Um viele str-Objekte zu akkumulieren, wird empfohlen, sie in eine Liste aufzunehmen und am Ende str.join () aufzurufen:

chunks = []
for s in my_strings:
    chunks.append(s)
result = ''.join(chunks)

(Eine andere einigermaßen effiziente Redewendung ist die Verwendung von io.StringIO)

Um viele Byte-Objekte zu akkumulieren, wird empfohlen, ein Bytearray-Objekt mithilfe der direkten Verkettung zu erweitern (der Operator + =):

result = bytearray()
for b in my_bytes_objects:
    result += b

Bearbeiten: Ich war albern und hatte die Ergebnisse rückwärts eingefügt, sodass es so aussah, als wäre das Anhängen an eine Liste schneller als cStringIO. Ich habe auch Tests für Bytearray / Str Concat sowie eine zweite Testrunde mit einer größeren Liste mit größeren Strings hinzugefügt. (Python 2.7.3)

IPython-Testbeispiel für große Listen von Zeichenfolgen

try:
    from cStringIO import StringIO
except:
    from io import StringIO

source = ['foo']*1000

%%timeit buf = StringIO()
for i in source:
    buf.write(i)
final = buf.getvalue()
# 1000 loops, best of 3: 1.27 ms per loop

%%timeit out = []
for i in source:
    out.append(i)
final = ''.join(out)
# 1000 loops, best of 3: 9.89 ms per loop

%%timeit out = bytearray()
for i in source:
    out += i
# 10000 loops, best of 3: 98.5 µs per loop

%%timeit out = ""
for i in source:
    out += i
# 10000 loops, best of 3: 161 µs per loop

## Repeat the tests with a larger list, containing
## strings that are bigger than the small string caching 
## done by the Python
source = ['foo']*1000

# cStringIO
# 10 loops, best of 3: 19.2 ms per loop

# list append and join
# 100 loops, best of 3: 144 ms per loop

# bytearray() +=
# 100 loops, best of 3: 3.8 ms per loop

# str() +=
# 100 loops, best of 3: 5.11 ms per loop
jdi
quelle
2
cStringIOexistiert nicht in Py3. Verwenden Sie io.StringIOstattdessen.
lvc
2
Warum das wiederholte Anhängen an eine Zeichenfolge teuer sein kann: joelonsoftware.com/articles/fog0000000319.html
Wes
36

In Python> = 3.6 ist die neue f-Zeichenfolge eine effiziente Möglichkeit, eine Zeichenfolge zu verketten.

>>> name = 'some_name'
>>> number = 123
>>>
>>> f'Name is {name} and the number is {number}.'
'Name is some_name and the number is 123.'
SuperNova
quelle
8

Die empfohlene Methode ist weiterhin das Anhängen und Verbinden.

MRAB
quelle
1
Wie Sie meiner Antwort entnehmen können, hängt dies davon ab, wie viele Zeichenfolgen Sie verketten. Ich habe einige Zeitangaben dazu gemacht (siehe den Vortrag, auf den ich in meinen Kommentaren zu meiner Antwort verwiesen habe) und im Allgemeinen, wenn es nicht mehr als zehn sind, + verwenden.
Lennart Regebro
1
PEP8 erwähnt dies ( python.org/dev/peps/pep-0008/#programming-recommendations ). Das Rationale ist, dass CPython zwar spezielle Optimierungen für die Verkettung von Zeichenfolgen mit + = hat, andere Implementierungen jedoch möglicherweise nicht.
Quantum7
8

Wenn die Saiten Sie verketten Literalen sind, verwenden Stringliteral Verkettung

re.compile(
        "[A-Za-z_]"       # letter or underscore
        "[A-Za-z0-9_]*"   # letter, digit or underscore
    )

Dies ist nützlich, wenn Sie einen Teil einer Zeichenfolge kommentieren möchten (wie oben) oder wenn Sie rohe Zeichenfolgen oder dreifache Anführungszeichen für einen Teil eines Literals verwenden möchten, jedoch nicht für alle.

Da dies auf der Syntaxebene geschieht, werden keine Verkettungsoperatoren verwendet.

Droide
quelle
7

Sie schreiben diese Funktion

def str_join(*args):
    return ''.join(map(str, args))

Dann können Sie einfach anrufen, wo immer Sie wollen

str_join('Pine')  # Returns : Pine
str_join('Pine', 'apple')  # Returns : Pineapple
str_join('Pine', 'apple', 3)  # Returns : Pineapple3
Shameem
quelle
1
str_join = lambda *str_list: ''.join(s for s in str_list)
Rick unterstützt Monica
7

Die Verwendung der direkten Verkettung von Zeichenfolgen durch '+' ist DIE SCHLECHTESTE Verkettungsmethode in Bezug auf Stabilität und Kreuzimplementierung, da nicht alle Werte unterstützt werden. PEP8-Standard rät davon ab und die Verwendung von format (), join () und append () für die langfristige Verwendung.

Wie aus dem verlinkten Abschnitt "Programmierempfehlungen" zitiert:

Verlassen Sie sich beispielsweise nicht auf CPythons effiziente Implementierung der direkten Zeichenfolgenverkettung für Anweisungen in der Form a + = b oder a = a + b. Diese Optimierung ist selbst in CPython fragil (funktioniert nur bei einigen Typen) und ist in Implementierungen ohne Refcounting überhaupt nicht vorhanden. In leistungsempfindlichen Teilen der Bibliothek sollte stattdessen das Formular '' .join () verwendet werden. Dadurch wird sichergestellt, dass die Verkettung in linearer Zeit über verschiedene Implementierungen hinweg erfolgt.

Badslacks
quelle
5
Referenzlink wäre schön gewesen :)
6

Während etwas veraltet, Code wie ein Pythonista: Idiomatic Python empfiehlt join()über die + in diesem Abschnitt . Ebenso wie PythonSpeedPerformanceTips in seinem Abschnitt zur Verkettung von Zeichenfolgen mit dem folgenden Haftungsausschluss:

Die Richtigkeit dieses Abschnitts ist in Bezug auf spätere Versionen von Python umstritten. In CPython 2.5 ist die Verkettung von Zeichenfolgen ziemlich schnell, obwohl dies möglicherweise nicht für andere Python-Implementierungen gilt. Weitere Informationen finden Sie unter ConcatenationTestCode.

Levon
quelle
6

Wie @jdi erwähnt, empfiehlt die Python-Dokumentation die Verwendung str.joinoder die io.StringIOVerkettung von Zeichenfolgen. Und sagt, dass ein Entwickler eine quadratische Zeit +=in einer Schleife erwarten sollte , obwohl es seit Python 2.4 eine Optimierung gibt. Wie diese Antwort sagt:

Wenn Python feststellt, dass das linke Argument keine anderen Referenzen enthält, wird reallocversucht, eine Kopie zu vermeiden, indem die Größe der Zeichenfolge geändert wird. Darauf sollten Sie sich niemals verlassen, da es sich um ein Implementierungsdetail handelt und reallocdie Leistung ohnehin auf O (n ^ 2) abnimmt, wenn die Zeichenfolge häufig verschoben werden muss.

Ich werde ein Beispiel für realen Code zeigen, der sich naiv auf +=diese Optimierung stützte , aber nicht zutraf. Der folgende Code konvertiert eine Iterable von kurzen Zeichenfolgen in größere Blöcke, die in einer Bulk-API verwendet werden.

def test_concat_chunk(seq, split_by):
    result = ['']
    for item in seq:
        if len(result[-1]) + len(item) > split_by: 
            result.append('')
        result[-1] += item
    return result

Dieser Code kann aufgrund der quadratischen Zeitkomplexität stundenlang literarisch ausgeführt werden. Nachfolgend finden Sie Alternativen mit vorgeschlagenen Datenstrukturen:

import io

def test_stringio_chunk(seq, split_by):
    def chunk():
        buf = io.StringIO()
        size = 0
        for item in seq:
            if size + len(item) <= split_by:
                size += buf.write(item)
            else:
                yield buf.getvalue()
                buf = io.StringIO()
                size = buf.write(item)
        if size:
            yield buf.getvalue()

    return list(chunk())

def test_join_chunk(seq, split_by):
    def chunk():
        buf = []
        size = 0
        for item in seq:
            if size + len(item) <= split_by:
                buf.append(item)
                size += len(item)
            else:
                yield ''.join(buf)                
                buf.clear()
                buf.append(item)
                size = len(item)
        if size:
            yield ''.join(buf)

    return list(chunk())

Und ein Mikro-Benchmark:

import timeit
import random
import string
import matplotlib.pyplot as plt

line = ''.join(random.choices(
    string.ascii_uppercase + string.digits, k=512)) + '\n'
x = []
y_concat = []
y_stringio = []
y_join = []
n = 5
for i in range(1, 11):
    x.append(i)
    seq = [line] * (20 * 2 ** 20 // len(line))
    chunk_size = i * 2 ** 20
    y_concat.append(
        timeit.timeit(lambda: test_concat_chunk(seq, chunk_size), number=n) / n)
    y_stringio.append(
        timeit.timeit(lambda: test_stringio_chunk(seq, chunk_size), number=n) / n)
    y_join.append(
        timeit.timeit(lambda: test_join_chunk(seq, chunk_size), number=n) / n)
plt.plot(x, y_concat)
plt.plot(x, y_stringio)
plt.plot(x, y_join)
plt.legend(['concat', 'stringio', 'join'], loc='upper left')
plt.show()

Mikro-Benchmark

saaj
quelle
5

Sie können auf verschiedene Arten tun.

str1 = "Hello"
str2 = "World"
str_list = ['Hello', 'World']
str_dict = {'str1': 'Hello', 'str2': 'World'}

# Concatenating With the + Operator
print(str1 + ' ' + str2)  # Hello World

# String Formatting with the % Operator
print("%s %s" % (str1, str2))  # Hello World

# String Formatting with the { } Operators with str.format()
print("{}{}".format(str1, str2))  # Hello World
print("{0}{1}".format(str1, str2))  # Hello World
print("{str1} {str2}".format(str1=str_dict['str1'], str2=str_dict['str2']))  # Hello World
print("{str1} {str2}".format(**str_dict))  # Hello World

# Going From a List to a String in Python With .join()
print(' '.join(str_list))  # Hello World

# Python f'strings --> 3.6 onwards
print(f"{str1} {str2}")  # Hello World

Ich habe diese kleine Zusammenfassung durch folgende Artikel erstellt.

Kushan Gunasekera
quelle
3

Mein Anwendungsfall war etwas anders. Ich musste eine Abfrage erstellen, bei der mehr als 20 Felder dynamisch waren. Ich folgte diesem Ansatz der Formatmethode

query = "insert into {0}({1},{2},{3}) values({4}, {5}, {6})"
query.format('users','name','age','dna','suzan',1010,'nda')

Dies war für mich vergleichsweise einfacher, anstatt + oder andere Methoden zu verwenden

Ishwar Rimal
quelle