¿Alguna razón para no usar '+' para concatenar dos cadenas?

123

Un antipatrón común en Python es concatenar una secuencia de cadenas usando +un bucle. Esto es malo porque el intérprete de Python tiene que crear un nuevo objeto de cadena para cada iteración, y termina tomando tiempo cuadrático. (Las versiones recientes de CPython aparentemente pueden optimizar esto en algunos casos, pero otras implementaciones no pueden, por lo que se desalienta a los programadores a confiar en esto). ''.joinEs la forma correcta de hacerlo.

Sin embargo, he oído decir ( incluido aquí en Stack Overflow ) que nunca, nunca debes usar +para la concatenación de cadenas, sino que siempre debes usar ''.joino formatear una cadena. No entiendo por qué este es el caso si solo estás concatenando dos cadenas. Si mi interpretación es correcta, no debe tomar tiempo cuadrática, y creo que a + bes más limpio y más fácil de leer que sea ''.join((a, b))o '%s%s' % (a, b).

¿Es una buena práctica usar +para concatenar dos cadenas? ¿O hay un problema que desconozco?

Taymon
fuente
Es más ordenado y tienes más control para no hacer concatenación. PERO es un poco más lento, intercambio de cuerdas: P
Jakob Bowyer
¿Estás diciendo que +es más rápido o más lento? ¿Y por qué?
Taymon
1
+ es más rápido, 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
44
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 y otros: el argumento "la concatenación de cadenas es mala" no tiene casi nada que ver con la velocidad, sino que aprovecha la conversión automática de tipos con __str__. Vea mi respuesta para ejemplos.
Izkata

Respuestas:

119

No hay nada de malo en concatenar dos cadenas con +. De hecho, es más fácil de leer que ''.join([a, b]).

Sin embargo, tiene razón en que concatenar más de 2 cadenas +es una operación O (n ^ 2) (en comparación con O (n) para join) y, por lo tanto, se vuelve ineficiente. Sin embargo, esto no tiene que ver con el uso de un bucle. Even a + b + c + ...es O (n ^ 2), la razón es que cada concatenación produce una nueva cadena.

CPython2.4 y superior intentan mitigar eso, pero aún es recomendable usarlo joincuando se concatenen más de 2 cadenas.

ggozad
fuente
55
@Mutant: .jointoma un iterable, por lo tanto .join([a,b])y .join((a,b))son válidas.
fundación
1
Tiempos interesantes sugieren utilizar +o +=en la respuesta aceptada (desde 2013) en stackoverflow.com/a/12171382/378826 (de Lennart Regebro) incluso para CPython 2.3+ y elegir el patrón "agregar / unir" si esto deja en claro idea para la solución del problema en cuestión.
Dilettant
49

El operador Plus es una solución perfecta para concatenar dos cadenas de Python. Pero si sigue agregando más de dos cadenas (n> 25), es posible que desee pensar en otra cosa.

''.join([a, b, c]) El truco es una optimización del rendimiento.

Mikko Ohtamaa
fuente
2
¿No sería mejor una tupla que una lista?
ThiefMaster
77
La tupla sería más rápida: el código era solo un ejemplo :) Por lo general, las entradas de cadenas múltiples largas son dinámicas.
Mikko Ohtamaa
55
@martineau Creo que se refiere a generar dinámicamente e append()incluir cadenas en una lista.
Peter C
55
Necesito decir aquí: la tupla generalmente es una estructura MÁS LENTA, especialmente si está creciendo. Con list puede usar list.extend (list_of_items) y list.append (item), que son mucho más rápidos al concatenar cosas dinámicamente.
Antti Haapala
66
+1 para n > 25. Los humanos necesitan puntos de referencia para comenzar en alguna parte.
n611x007
8

La suposición de que uno nunca debe usar + para la concatenación de cadenas, sino que siempre debe usar '' .join puede ser un mito. Es cierto que el uso +crea copias temporales innecesarias de objetos de cadena inmutables, pero el otro hecho no mencionado es que llamar joina un bucle generalmente agregaría la sobrecarga de function call. Tomemos tu ejemplo.

Cree dos listas, una a partir de la pregunta SO vinculada y otra más grande fabricada

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

Vamos a crear dos funciones, UseJoiny UsePlususar las respectivas joiny +funcionalidades.

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

Vamos a correr el tiempo con la primera lista

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

Tienen casi el mismo tiempo de ejecución.

Vamos a usar 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}

Y parece que el uso de Join da como resultado llamadas de función innecesarias que podrían aumentar la sobrecarga.

Ahora volviendo a la pregunta. ¿Se debe desalentar el uso de +over joinen todos los casos?

Creo que no, las cosas deben tomarse en consideración

  1. Longitud de la cadena en cuestión
  2. No de operación de concatenación.

Y, por supuesto, en un desarrollo, la optimización prematura es malvada.

Abhijit
fuente
77
Por supuesto, la idea sería no usar joindentro del propio ciclo, sino que el ciclo generaría una secuencia que se pasaría para unirse.
jsbueno
7

Cuando se trabaja con varias personas, a veces es difícil saber exactamente qué está sucediendo. El uso de una cadena de formato en lugar de concatenación puede evitar una molestia particular que nos ha sucedido muchas veces:

Digamos, una función requiere un argumento, y lo escribes esperando obtener una cadena:

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

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

Por lo tanto, esta función se puede usar con bastante frecuencia en todo el código. Es posible que sus compañeros de trabajo sepan exactamente lo que hace, pero no necesariamente estén completamente actualizados en lo interno, y pueden no saber que la función espera una cadena. Y entonces pueden terminar con esto:

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

No habría ningún problema si solo utilizara una cadena de formato:

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

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

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

Lo mismo es cierto para todos los tipos de objetos que definen __str__, que también pueden pasarse:

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

Entonces sí: si puede usar una cadena de formato, hágalo y aproveche lo que Python tiene para ofrecer.

Izkata
fuente
1
+1 para una opinión disidente bien razonada. Aunque todavía creo que estoy a favor +.
Taymon
1
¿Por qué no definirías el método foo como: print 'bar:' + str (zeta)?
EngineerWithJava54321
@ EngineerWithJava54321 Por un ejemplo, zeta = u"a\xac\u1234\u20ac\U00008000"por lo que tendría que usar print 'bar: ' + unicode(zeta)para asegurarse de que no se produzca un error. %slo hace bien sin tener que pensarlo, y es mucho más corto
Izkata
@ EngineerWithJava54321 Otros ejemplos son menos relevantes aquí, pero por ejemplo, "bar: %s"podrían traducirse a "zrb: %s br"algún otro idioma. La %sversión simplemente funcionará, pero la versión string-concat se convertiría en un desastre para manejar todos los casos y sus traductores ahora tendrían dos traducciones separadas para tratar
Izkata
Si no saben cuál es la implementación de foo, se encontrarán con este error con cualquiera def.
dentro del
3

He hecho una prueba rápida:

import sys

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

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

y cronometrado:

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

Aparentemente hay una optimización para el a = a + bcaso. No exhibe el tiempo O (n ^ 2) como se podría sospechar.

Entonces, al menos en términos de rendimiento, el uso +está bien.

Michael Slade
fuente
3
Puede compararlo con el caso de "unirse" aquí. Y está el asunto de otras implementaciones de Python, tales como pypy, jython, ironpython, etc ...
jsbueno
3

De acuerdo con los documentos de Python, el uso de str.join () le dará consistencia de rendimiento en varias implementaciones de Python. Aunque CPython optimiza el comportamiento cuadrático de s = s + t, otras implementaciones de Python pueden no hacerlo.

Detalle de implementación de CPython : si syt son ambas cadenas, algunas implementaciones de Python como CPython generalmente pueden realizar una optimización en el lugar para las asignaciones de la forma s = s + t o s + = t. Cuando corresponde, esta optimización hace que el tiempo de ejecución cuadrático sea mucho menos probable. Esta optimización depende tanto de la versión como de la implementación. Para el código sensible al rendimiento, es preferible utilizar el método str.join () que asegura un rendimiento de concatenación lineal consistente en todas las versiones e implementaciones.

Tipos de secuencia en documentos de Python (consulte la nota al pie [6])

Duque
fuente
2

Yo uso lo siguiente con Python 3.8

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

'' .join ([a, b]) es una mejor solución que + .

Porque el Código debe escribirse de manera que no perjudique a otras implementaciones de Python (PyPy, Jython, IronPython, Cython, Psyco y demás)

formar un + = b o a = a + b es frágil incluso en CPython y no está presente en absoluto en las implementaciones que no utilizan refcounting (recuento de referencias es una técnica de almacenar el número de referencias, indicadores, o las manijas a una recurso como un objeto, bloque de memoria, espacio en disco u otro recurso )

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

muhammad ali e
fuente
1
a += bfunciona en todas las implementaciones de Python, es solo que en algunas de ellas toma tiempo cuadrático cuando se realiza dentro de un ciclo ; La pregunta era sobre la concatenación de cadenas fuera de un bucle.
Taymon