Qualche motivo per non usare "+" per concatenare due stringhe?


124

Un antipattern comune in Python consiste nel concatenare una sequenza di stringhe utilizzando +in un ciclo. Questo è negativo perché l'interprete Python deve creare un nuovo oggetto stringa per ogni iterazione e finisce per impiegare un tempo quadratico. (Le versioni recenti di CPython possono apparentemente ottimizzarlo in alcuni casi, ma altre implementazioni no, quindi i programmatori sono scoraggiati dall'affidarsi a questo.) ''.joinÈ il modo giusto per farlo.

Tuttavia, ho sentito dire ( anche qui su Stack Overflow ) che non dovresti mai, mai usare +per la concatenazione di stringhe, ma invece usare sempre ''.joino una stringa di formato. Non capisco perché questo sia il caso se stai solo concatenando due stringhe. Se la mia comprensione è corretta, non dovrebbe richiedere tempo quadratico, e penso che a + bsia più pulito e più leggibile di ''.join((a, b))o '%s%s' % (a, b).

È buona norma utilizzare +per concatenare due stringhe? O c'è un problema di cui non sono a conoscenza?


È più ordinato e hai più controllo per non fare concatenazioni. MA è un compromesso leggermente più lento, che colpisce le corde: P
Jakob Bowyer

Stai dicendo che +è più veloce o più lento? E perché?
Taymon

1
+ è più veloce, 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 e altri: L'argomento "concatenazione di stringhe è pessimo" non ha quasi nulla a che fare con la velocità, ma si avvale della conversione automatica del tipo con __str__. Vedi la mia risposta per esempi.
Izkata

Risposte:


120

Non c'è niente di sbagliato nel concatenare due stringhe con +. In effetti è più facile da leggere di ''.join([a, b]).

Hai ragione però che concatenare più di 2 stringhe con + è un'operazione O (n ^ 2) (rispetto a O (n) per join) e quindi diventa inefficiente. Tuttavia questo non ha a che fare con l'utilizzo di un ciclo. Anche a + b + c + ...è O (n ^ 2), il motivo è che ogni concatenazione produce una nuova stringa.

CPython2.4 e versioni successive cercano di mitigarlo, ma è comunque consigliabile utilizzarlo joinquando si concatenano più di 2 stringhe.


5
@Mutant: .joinaccetta un iterabile, quindi entrambi .join([a,b])e .join((a,b))sono validi.
trovatello il

1
Interessanti tempi suggeriscono l'utilizzo +o +=nella risposta accettata (dal 2013) su stackoverflow.com/a/12171382/378826 (da Lennart Regebro) anche per CPython 2.3+ e per scegliere il modello "append / join" solo se questo più chiaro espone il idea per la soluzione del problema a portata di mano.
Dilettant

49

L'operatore Plus è una soluzione perfetta per concatenare due stringhe Python. Ma se continui ad aggiungere più di due stringhe (n> 25), potresti pensare a qualcos'altro.

''.join([a, b, c]) il trucco è un'ottimizzazione delle prestazioni.


2
Una tupla non sarebbe meglio di una lista?
ThiefMaster

7
La tupla sarebbe più veloce - il codice era solo un esempio :) Di solito gli input di stringhe multiple lunghe sono dinamici.
Mikko Ohtamaa

5
@martineau Penso che intenda generare dinamicamente e inserire append()stringhe in un elenco.
Peter C

5
C'è da dire qui: la tupla di solito è una struttura LENTA, specialmente se è in crescita. Con list puoi usare list.extend (list_of_items) e list.append (item) che sono molto più veloci quando concatenano cose dinamicamente.
Antti Haapala,

6
+1 per n > 25. Gli esseri umani hanno bisogno di punti di riferimento per iniziare da qualche parte.
n611x007

8

L'assunto che non si dovrebbe mai, mai usare + per la concatenazione di stringhe, ma invece usare sempre '' .join potrebbe essere un mito. È vero che using +crea copie temporanee non necessarie di oggetti stringa immutabili ma l'altro fatto non spesso citato è che la chiamatajoin in un ciclo in genere aggiungerebbe l'overhead di function call. Prendiamo il tuo esempio.

Crea due elenchi, uno dalla domanda SO collegata e un altro fabbricato più grande

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

Consente di creare due funzioni UseJoine UsePlusdi utilizzare le rispettivejoin e +funzionalità.

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

Consente di eseguire timeit con il primo elenco

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

Hanno quasi lo stesso tempo di esecuzione.

Consente di utilizzare 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}

E sembra che l'utilizzo di Join, si traduca in chiamate di funzioni non necessarie che potrebbero aumentare l'overhead.

Ora tornando alla domanda. Si dovrebbe scoraggiare l'uso di +overjoin in tutti i casi?

Credo di no, le cose dovrebbero essere prese in considerazione

  1. Lunghezza della stringa in questione
  2. No dell'operazione di concatenazione.

E fuori rotta in uno sviluppo l'ottimizzazione prematura è un male.


7
Ovviamente, l'idea sarebbe di non usare joinall'interno del ciclo stesso, piuttosto il ciclo genererebbe una sequenza che sarebbe passata per unirsi.
jsbueno

7

Quando si lavora con più persone, a volte è difficile sapere esattamente cosa sta succedendo. L'uso di una stringa di formato invece della concatenazione può evitare un particolare fastidio che ci è accaduto un sacco di volte:

Diciamo, una funzione richiede un argomento e lo scrivi aspettandoti di ottenere una stringa:

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

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

Quindi, questa funzione può essere utilizzata abbastanza spesso in tutto il codice. I tuoi colleghi potrebbero sapere esattamente cosa fa, ma non necessariamente essere completamente al passo con i tempi e potrebbero non sapere che la funzione si aspetta una stringa. E quindi potrebbero finire con questo:

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

Non ci sarebbero problemi se usassi solo una stringa di formato:

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

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

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

Lo stesso vale per tutti i tipi di oggetti che definiscono __str__, che possono anche essere passati:

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

Quindi sì: se puoi usare una stringa di formato fallo e approfitta di ciò che Python ha da offrire.


1
+1 per un'opinione dissenziente ben motivata. Penso ancora di essere favorevole +però.
Taymon

1
Perché non definire il metodo foo semplicemente come: print 'bar:' + str (zeta)?
EngineerWithJava54321

@ EngineerWithJava54321 Per un esempio, zeta = u"a\xac\u1234\u20ac\U00008000"quindi dovresti usare print 'bar: ' + unicode(zeta)per assicurarti che non si verifichino errori. %sfa bene senza
doverci

@ EngineerWithJava54321 Altri esempi sono meno rilevanti qui, ma ad esempio, "bar: %s"potrebbero essere tradotti "zrb: %s br"in un'altra lingua. La %sversione funzionerà, ma la versione concatenata di stringhe diventerebbe un pasticcio per gestire tutti i casi ei tuoi traduttori ora avrebbero due traduzioni separate da affrontare
Izkata

Se non sanno quale sia l'implementazione di foo, si imbatteranno in questo errore con any def.
all'interno del

3

Ho fatto un rapido test:

import sys

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

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

e cronometrato:

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

Apparentemente c'è un'ottimizzazione per il a = a + bcaso. Non mostra tempo O (n ^ 2) come si potrebbe sospettare.

Quindi, almeno in termini di prestazioni, l'uso +va bene.


3
Puoi confrontare qui il caso "join". E c'è la questione di altre implementazioni Python, come pypy, jython, ironpython, ecc ...
jsbueno

3

Secondo la documentazione di Python, l'uso di str.join () ti darà la coerenza delle prestazioni tra le varie implementazioni di Python. Sebbene CPython ottimizzi il comportamento quadratico di s = s + t, altre implementazioni di Python potrebbero non farlo.

Dettagli sull'implementazione di CPython : se set sono entrambe stringhe, alcune implementazioni Python come CPython possono solitamente eseguire un'ottimizzazione sul posto per gli assegnamenti della forma s = s + t o s + = t. Quando applicabile, questa ottimizzazione rende il tempo di esecuzione quadratico molto meno probabile. Questa ottimizzazione dipende sia dalla versione che dall'implementazione. Per il codice sensibile alle prestazioni, è preferibile utilizzare il metodo str.join () che garantisce prestazioni di concatenazione lineare coerenti tra versioni e implementazioni.

Tipi di sequenza nei documenti Python (vedere la nota a piè di pagina [6])


2

Uso quanto segue con Python 3.8

string4 = f'{string1}{string2}{string3}'

0

'' .join ([a, b]) è una soluzione migliore di + .

Perché il codice dovrebbe essere scritto in un modo che non svantaggi altre implementazioni di Python (PyPy, Jython, IronPython, Cython, Psyco e simili)

form a + = b o a = a + b è fragile anche in CPython e non è affatto presente nelle implementazioni che non usano refcounting (il conteggio dei riferimenti è una tecnica per memorizzare il numero di riferimenti, puntatori o handle a un risorsa come un oggetto, blocco di memoria, spazio su disco o altra risorsa )

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


1
a += bfunziona in tutte le implementazioni di Python, è solo che su alcune di esse ci vuole un tempo quadratico se fatto all'interno di un ciclo ; la domanda riguardava la concatenazione di stringhe al di fuori di un ciclo.
Taymon
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.