Qual è il metodo di concatenazione di stringhe più efficiente in Python?


148

Esiste un metodo di concatenazione di stringhe di massa efficiente in Python (come StringBuilder in C # o StringBuffer in Java)? Ho trovato i seguenti metodi qui :

  • Semplice concatenazione usando +
  • Utilizzo dell'elenco e del joinmetodo delle stringhe
  • Utilizzando UserStringdal MutableStringmodulo
  • Utilizzo dell'array di caratteri e del arraymodulo
  • Utilizzando cStringIOdal StringIOmodulo

Ma cosa usano o suggeriscono voi esperti, e perché?

[ Una domanda correlata qui ]



Per concatenare frammenti noti in uno, Python 3.6 avrà f''stringhe di formato che saranno più veloci di qualsiasi alternativa nelle precedenti versioni di Python.
Antti Haapala,

Risposte:


127

Potresti essere interessato a questo: un aneddoto sull'ottimizzazione di Guido. Anche se vale la pena ricordare anche che questo è un vecchio articolo e precede l'esistenza di cose del genere ''.join(anche se immagino string.joinfieldssia più o meno lo stesso)

In base a ciò, il arraymodulo potrebbe essere più veloce se riesci a risolvere il tuo problema. Ma ''.joinè probabilmente abbastanza veloce e ha il vantaggio di essere idiomatico e quindi più facile da capire per altri programmatori di Python.

Infine, la regola d'oro dell'ottimizzazione: non ottimizzare a meno che tu non sappia che devi, e misura invece di indovinare.

Puoi misurare diversi metodi usando il timeitmodulo. Ciò può dirti quale è il più veloce, invece di estranei casuali su Internet che fanno ipotesi.


1
Volendo aggiungere il punto su quando ottimizzare: assicurati di testare nei casi peggiori. Ad esempio, posso aumentare il mio campione in modo che il mio codice corrente passi da 0,17 secondi a 170 secondi. Beh, voglio testare su campioni di dimensioni più grandi poiché c'è meno variazione lì.
Flipper

2
"Non ottimizzare fino a quando non sai che è necessario." A meno che tu non stia semplicemente usando un linguaggio nominale diverso e puoi evitare di rielaborare il tuo codice con un piccolo sforzo aggiuntivo.
jeremyjjbrown,

1
Un posto in cui sai di aver bisogno è l'intervista (che è sempre un ottimo momento per ripulire la tua profonda comprensione). Purtroppo non ho trovato NESSUN articolo moderno a riguardo. (1) Java / C # String è ancora così male nel 2017? (2) Che ne dici di C ++? (3) Ora parla dell'ultimo e più grande in Python, focalizzandoci sui casi in cui dobbiamo fare milioni di concatenazioni. Possiamo fidarci che il join funzionerebbe in tempo lineare?
user1854182

Cosa significa "abbastanza veloce" per .join()? La domanda principale è, se a) creare una copia della stringa per la concatenazione (simile a s = s + 'abc'), che richiede O (n) runtime, oppure b) semplicemente aggiungere alla stringa esistente senza creare una copia, che richiede O (1) ?
CGFoX,

64

''.join(sequenceofstrings) è ciò che di solito funziona meglio, il più semplice e veloce.


3
@mshsayem, in Python una sequenza può essere qualsiasi oggetto enumerabile, persino una funzione.
Nick Dandoulakis,

2
Adoro il ''.join(sequence)linguaggio. È particolarmente utile per produrre elenchi separati da virgole: ', '.join([1, 2, 3])fornisce la stringa '1, 2, 3'.
Andrew Keeton,

7
@mshsayem: "".join(chr(x) for x in xrange(65,91))--- in questo caso, l'argomento da unire è un iteratore, creato attraverso un'espressione del generatore. Non esiste un elenco temporaneo che viene creato.
balpha,

2
@balpha: eppure la versione del generatore è più lenta della versione di comprensione dell'elenco: C: \ temp> python -mtimeit "'' .join (chr (x) per x in xrange (65,91))" 100000 loop, meglio di 3: 9.71 usec per loop C: \ temp> python -mtimeit "'' .join ([chr (x) per x in xrange (65,91)])" 100000 loop, meglio di 3: 7.1 usec per loop
hughdbrown

1
@hughdbrown, sì, quando hai memoria libera fuori il wazoo (tipico caso timeit) listcomp può essere ottimizzato meglio di genexp, spesso del 20-30%. Quando le cose strette della memoria sono diverse - difficile da riprodurre in tempo! -)
Alex Martelli

58

Python 3.6 ha cambiato il gioco per la concatenazione di stringhe di componenti noti con interpolazione letterale di stringhe .

Dato il caso di prova da risposta di mkoistinen , con stringhe

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

I contendenti lo sono

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

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

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

  • ''.join(('http://', domain, '/', lang, '/', path))- 0,249 µs (notare che la creazione di una tupla di lunghezza costante è leggermente più rapida rispetto alla creazione di un elenco di lunghezza costante).

Quindi attualmente il codice più breve e più bello possibile è anche il più veloce.

Nelle versioni alfa di Python 3.6 l'implementazione delle f''stringhe era la più lenta possibile - in realtà il codice byte generato è praticamente equivalente al ''.join()caso con chiamate non necessarie a str.__format__cui senza argomenti tornerebbeself invariato. Queste inefficienze sono state affrontate prima del 3.6 finale.

La velocità può essere contrastata con il metodo più veloce per Python 2, che è la +concatenazione sul mio computer; e ciò richiede 0,203 µs con stringhe a 8 bit e 0,259 µs se le stringhe sono tutte Unicode.


38

Dipende da cosa stai facendo.

Dopo Python 2.5, la concatenazione di stringhe con l'operatore + è piuttosto veloce. Se stai solo concatenando un paio di valori, usare l'operatore + funziona meglio:

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

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

Tuttavia, se stai mettendo insieme una stringa in un ciclo, è meglio usare il metodo di unione dell'elenco:

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

... ma nota che devi mettere insieme un numero relativamente alto di stringhe prima che la differenza diventi evidente.


2
1) Nella tua prima misurazione è probabilmente la costruzione dell'elenco che richiede tempo. Prova con una tupla. 2) CPython funziona in modo uniforme, tuttavia altre implementazioni di Python funzionano in modo peggiore con + e + =
u0b34a0f6ae

22

Come per la risposta di John Fouhy, non lo fanno ottimizzare meno che non bisogna, ma se siete qui e fare questa domanda, potrebbe essere proprio perché si deve . Nel mio caso, avevo bisogno di assemblare alcuni URL da variabili stringa ... velocemente. Ho notato che nessuno (finora) sembra prendere in considerazione il metodo del formato di stringa, quindi ho pensato di provarlo e, soprattutto per un lieve interesse, ho pensato di lanciare l'operatore di interpolazione delle stringhe per un buon misuratore. Ad essere sincero, non pensavo che nessuno di questi si sarebbe impilato in un'operazione diretta '+' o in '' .join (). Ma indovina un po? Sul mio sistema Python 2.7.5, l'operatore di interpolazione delle stringhe le governa tutte e string.format () è il peggiore:

# 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), ))

I risultati:

# 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)

Se uso un dominio più breve e un percorso più breve, l'interpolazione vince comunque. La differenza è più pronunciata, tuttavia, con stringhe più lunghe.

Ora che ho avuto un bel test script, ho anche testato in Python 2.6, 3.3 e 3.4, ecco i risultati. In Python 2.6, l'operatore plus è il più veloce! Su Python 3, l'adesione vince. Nota: questi test sono molto ripetibili sul mio sistema. Quindi, 'plus' è sempre più veloce su 2.6, 'intp' è sempre più veloce su 2.7 e 'join' è sempre più veloce su Python 3.x.

# 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)

Lezione imparata:

  • A volte, i miei presupposti sono completamente sbagliati.
  • Test contro il sistema env. sarai in esecuzione in produzione.
  • L'interpolazione delle stringhe non è ancora morta!

tl; dr:

  • Se si utilizza 2.6, utilizzare l'operatore +.
  • se stai usando 2.7 usa l'operatore '%'.
  • se stai usando 3.x usa '' .join ().

2
Nota: l'interpolazione letterale delle stringhe è ancora più veloce per 3.6+:f'http://{domain}/{lang}/{path}'
TemporalWolf

1
Inoltre, .format()ha tre forme, in ordine da veloce a lento:"{}".format(x) , "{0}".format(x),"{x}".format(x=x)
TemporalWolf

La vera lezione: quando il tuo dominio problematico è piccolo, ad esempio la composizione di stringhe brevi, il metodo molto spesso non ha importanza. E anche quando è importante, ad esempio se stai davvero costruendo un milione di stringhe, il sovraccarico spesso conta di più. È un sintomo tipico di preoccuparsi del problema sbagliato. Solo quando l'overhead non è significativo, ad esempio quando si crea l'intero libro come una stringa, la differenza di metodo inizia a contare.
Hui Zhou,

7

dipende praticamente dalle dimensioni relative della nuova stringa dopo ogni nuova concatenazione. Con l' +operatore, per ogni concatenazione viene creata una nuova stringa. Se le stringhe intermedie sono relativamente lunghe, il+ diventano sempre più lente perché viene memorizzata la nuova stringa intermedia.

Considera questo caso:

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

risultati

1 0,00493192672729

2 0.000509023666382

3 0.00042200088501

4 0.000482797622681

Nel caso di 1 e 2, aggiungiamo una stringa grande e join () esegue circa 10 volte più velocemente. Nel caso 3 e 4, aggiungiamo una piccola stringa e '+' si comporta leggermente più velocemente


3

Mi sono imbattuto in una situazione in cui avevo bisogno di avere una stringa appendibile di dimensioni sconosciute. Questi sono i risultati del benchmark (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

Questo sembra mostrare che '+ =' è il più veloce. I risultati del link skymind sono un po 'obsoleti.

(Mi rendo conto che il secondo esempio non è completo, l'elenco finale avrebbe bisogno di essere unito. Ciò dimostra, tuttavia, che la semplice preparazione dell'elenco richiede più tempo del concat per stringhe.)


Ricevo sub 1 secondo per i test 3 ° e 4 °. Perché stai ottenendo tempi così alti? pastebin.com/qabNMCHS
bad_keypoints

@ronnieaka: sta ottenendo tempi inferiori a 1 secondo per tutti i test. Sta ricevendo> 1 µs per il 3 ° e il 4 °, cosa che non è stata fatta. Su quei test ottengo anche tempi più lenti (su Python 2.7.5, Linux). Potrebbe essere CPU, versione, costruire flag, chi lo sa.
Thanatos,

Questi risultati di benchmark sono inutili. In particolare, il primo caso, che non esegue alcuna concatenazione di stringhe, restituisce intatto il secondo valore di stringa.
Antti Haapala,

3

Un anno dopo, testiamo la risposta di mkoistinen con Python 3.4.3:

  • più 0.963564149000 (il 95.83% più veloce)
  • iscriviti a 0.923408469000 (100.00% più veloce)
  • modulo 1.501130934000 (61,51% più veloce)
  • intp 1.019677452000 (90.56% più veloce)

Niente è cambiato. Unirsi è ancora il metodo più veloce. Con intp è senza dubbio la scelta migliore in termini di leggibilità, potresti comunque voler usare intp.


1
Forse potrebbe essere un'aggiunta alla risposta mkoistinen poiché è un po 'a corto di una risposta completa (o almeno aggiungi il codice che stai utilizzando).
Trilarion,

1

Ispirato ai benchmark di @ JasonBaker, eccone uno semplice che confronta 10 "abcdefghijklmnopqrstuvxyz"stringhe, dimostrando che .join()è più veloce; anche con questo piccolo aumento delle variabili:

Catenation

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

Aderire

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

Date un'occhiata alla risposta accettata (scorrere verso il basso lungo) di questa domanda: stackoverflow.com/questions/1349311/...
mshsayem

1

Per un piccolo set di stringhe brevi (ovvero 2 o 3 stringhe di non più di pochi caratteri), il plus è ancora molto più veloce. Usando il meraviglioso script di mkoistinen in Python 2 e 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)

Quindi, quando il tuo codice sta facendo un numero enorme di piccole concatenazioni separate, in più è il modo preferito se la velocità è cruciale.


1

Probabilmente "nuove stringhe f in Python 3.6" è il modo più efficiente di concatenare le stringhe.

Utilizzando% s

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

Utilizzando .format

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

Usando f

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

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.