¿Cuál es el método de concatenación de cadenas más eficiente en python?

148

¿Existe algún método eficiente de concatenación de cadenas masivas en Python (como StringBuilder en C # o StringBuffer en Java)? Encontré los siguientes métodos aquí :

  • Concatenación simple usando +
  • Usando la lista de cadenas y el joinmétodo
  • Usando UserStringdesde el MutableStringmódulo
  • Usando la matriz de caracteres y el arraymódulo
  • Usando cStringIOdesde el StringIOmódulo

Pero, ¿qué usan o sugieren los expertos y por qué?

[ Una pregunta relacionada aquí ]

mshsayem
fuente
1
Pregunta similar: stackoverflow.com/questions/476772
Peter Mortensen
Para concatenar fragmentos conocidos en uno, Python 3.6 tendrá f''cadenas de formato que serán más rápidas que cualquier alternativa en versiones anteriores de Python.
Antti Haapala

Respuestas:

127

Te puede interesar esto: una anécdota de optimización de Guido. Aunque vale la pena recordar también que este es un artículo antiguo y es anterior a la existencia de cosas como ''.join(aunque supongo que string.joinfieldses más o menos lo mismo)

En virtud de eso, el arraymódulo puede ser más rápido si puede resolver su problema. Pero ''.joinprobablemente sea lo suficientemente rápido y tenga la ventaja de ser idiomático y, por lo tanto, más fácil de entender para otros programadores de Python.

Finalmente, la regla de oro de la optimización: no optimices a menos que sepas que lo necesitas y mide en lugar de adivinar.

Puede medir diferentes métodos usando el timeitmódulo. Eso le puede decir cuál es el más rápido, en lugar de extraños al azar en Internet haciendo conjeturas.

John Fouhy
fuente
1
Queriendo agregar al punto sobre cuándo optimizar: asegúrese de probar contra los peores casos. Por ejemplo, puedo aumentar mi muestra para que mi código actual pase de 0.17 segundos a 170 segundos. Bueno, quiero probar con tamaños de muestra más grandes ya que hay menos variación allí.
Flipper
2
"No optimices hasta que sepas que lo necesitas". A menos que solo esté usando un idioma nominalmente diferente y pueda evitar el reprocesamiento de su código con poco esfuerzo adicional.
jeremyjjbrown
1
Un lugar que sabe que necesita es la entrevista (que siempre es un buen momento para repasar su comprensión profunda). Lamentablemente no he encontrado ningún artículo moderno sobre esto. (1) ¿Java / C # String sigue siendo tan malo en 2017? (2) ¿Qué tal C ++? (3) Ahora cuente sobre lo último y lo mejor de Python enfocándose en casos en los que necesitamos hacer millones de concatenaciones. ¿Podemos confiar en que join funcionaría en tiempo lineal?
usuario1854182
¿Qué significa "lo suficientemente rápido" para .join()? La pregunta principal es, ¿a) crea una copia de la cadena para la concatenación (similar a s = s + 'abc'), que requiere O (n) tiempo de ejecución, o b) simplemente agrega a la cadena existente sin crear una copia, que requiere O (1) ?
CGFoX
64

''.join(sequenceofstrings) es lo que generalmente funciona mejor: más simple y más rápido.

Alex Martelli
fuente
3
@mshsayem, en Python una secuencia puede ser cualquier objeto enumerable, incluso una función.
Nick Dandoulakis
2
Absolutamente amo el ''.join(sequence)idioma. Es especialmente útil producir listas separadas por comas: ', '.join([1, 2, 3])da la cadena '1, 2, 3'.
Andrew Keeton el
77
@mshsayem: "".join(chr(x) for x in xrange(65,91))--- en este caso, el argumento para unirse es un iterador, creado a través de una expresión generadora. No hay una lista temporal que se construya.
balpha
2
@balpha: y, sin embargo, la versión del generador es más lenta que la versión de comprensión de la lista: C: \ temp> python -mtimeit "'' .join (chr (x) para x en xrange (65,91))" 100000 bucles, lo mejor de 3: 9.71 usec por bucle C: \ temp> python -mtimeit "'' .join ([chr (x) para x en xrange (65,91)])" 100000 bucles, lo mejor de 3: 7.1 usec por bucle
hughdbrown
1
@hughdbrown, sí, cuando tiene memoria libre, el wazoo (caso típico de timeit) listcomp puede optimizarse mejor que genexp, a menudo en un 20-30%. Cuando las cosas apretadas de memoria son diferentes - es difícil de reproducir en timeit, aunque -!)
Alex Martelli
58

Python 3.6 cambió el juego para la concatenación de cadenas de componentes conocidos con Literal String Interpolation .

Dado el caso de prueba de la respuesta de mkoistinen , tener cadenas

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

Los contendientes son

  • 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 (observe que construir una tupla de longitud constante es un poco más rápido que construir una lista de longitud constante).

Por lo tanto, actualmente el código más corto y hermoso posible es también el más rápido.

En las versiones alfa de Python 3.6, la implementación de f''cadenas fue la más lenta posible: en realidad, el código de bytes generado es prácticamente equivalente al ''.join()caso de llamadas innecesarias a las str.__format__que sin argumentos simplemente regresarían selfsin cambios. Estas ineficiencias se abordaron antes de 3.6 final.

La velocidad se puede contrastar con el método más rápido para Python 2, que es la +concatenación en mi computadora; y eso toma 0.203 µs con cadenas de 8 bits, y 0.259 µs si las cadenas son todas Unicode.

Antti Haapala
fuente
38

Depende de lo que estés haciendo.

Después de Python 2.5, la concatenación de cadenas con el operador + es bastante rápida. Si solo está concatenando un par de valores, usar el operador + funciona mejor:

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

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

Sin embargo, si está armando una cadena en un bucle, es mejor usar el método de unión de lista:

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

... pero tenga en cuenta que debe juntar un número relativamente alto de cadenas antes de que la diferencia sea notable.

Jason Baker
fuente
2
1) En su primera medición, es probable que la construcción de la lista lleve su tiempo. Prueba con una tupla. 2) CPython funciona uniformemente bien, sin embargo, otras implementaciones de Python funcionan mucho peor con + y + =
u0b34a0f6ae
22

Según la respuesta de John Fouhy, no optimices a menos que tengas que hacerlo, pero si estás aquí y haces esta pregunta, puede ser precisamente porque tienes que hacerlo . En mi caso, necesitaba ensamblar algunas URL de variables de cadena ... rápido. Noté que nadie (hasta ahora) parece estar considerando el método de formato de cadena, por lo que pensé en probarlo y, sobre todo por un leve interés, pensé en tirar el operador de interpolación de cadena allí para una buena medición. Para ser honesto, no pensé que ninguno de estos se apilara a una operación directa '+' o un '' .join (). ¿Pero adivina que? En mi sistema Python 2.7.5, el operador de interpolación de cadenas los gobierna a todos y string.format () es el de peor desempeño:

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

Los resultados:

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

Si uso un dominio más corto y una ruta más corta, la interpolación sigue ganando. Sin embargo, la diferencia es más pronunciada con cadenas más largas.

Ahora que tenía un buen script de prueba, también probé en Python 2.6, 3.3 y 3.4, aquí están los resultados. ¡En Python 2.6, el operador plus es el más rápido! En Python 3, unirse gana. Nota: estas pruebas son muy repetibles en mi sistema. Entonces, 'plus' siempre es más rápido en 2.6, 'intp' siempre es más rápido en 2.7 y 'join' siempre es más rápido en 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)

Lección aprendida:

  • A veces, mis suposiciones están completamente equivocadas.
  • Prueba contra el sistema env. estarás corriendo en producción.
  • ¡La interpolación de cuerdas aún no está muerta!

tl; dr:

  • Si usa 2.6, use el operador +.
  • si está utilizando 2.7, use el operador '%'.
  • si está utilizando 3.x use '' .join ().
mkoistinen
fuente
2
Nota: la interpolación de cadenas literales es aún más rápida para 3.6+:f'http://{domain}/{lang}/{path}'
TemporalWolf
1
Además, .format()tiene tres formas, con el fin de rápido a lento: "{}".format(x), "{0}".format(x),"{x}".format(x=x)
TemporalWolf
La verdadera lección: cuando el dominio de su problema es pequeño, por ejemplo, componer cadenas cortas, el método más a menudo no importa. E incluso cuando es importante, por ejemplo, si realmente está construyendo un millón de cadenas, los gastos generales a menudo son más importantes. Es un síntoma típico de preocuparse por el problema incorrecto. Solo cuando la sobrecarga no es significativa, por ejemplo, al construir un libro completo como una cadena, la diferencia de método comienza a importar.
Hui Zhou
7

depende en gran medida de los tamaños relativos de la nueva cadena después de cada nueva concatenación. Con el +operador, para cada concatenación se crea una nueva cadena. Si las cadenas intermedias son relativamente largas, se +vuelven cada vez más lentas porque se está almacenando la nueva cadena intermedia.

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

Resultados

1 0.00493192672729

2 0.000509023666382

3 0.00042200088501

4 0.000482797622681

En el caso de 1 y 2, agregamos una cadena grande y join () realiza aproximadamente 10 veces más rápido. En los casos 3 y 4, agregamos una cadena pequeña y '+' funciona un poco más rápido

David Bielen
fuente
3

Me encontré con una situación en la que necesitaba tener una cadena de tamaño desconocido. Estos son los resultados de referencia (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

Esto parece mostrar que '+ =' es el más rápido. Los resultados del enlace skymind están un poco desactualizados.

(Me doy cuenta de que el segundo ejemplo no está completo, la lista final necesitaría unirse. Sin embargo, esto muestra que simplemente preparar la lista lleva más tiempo que la cadena concat.)

MattK
fuente
Estoy obteniendo sub 1 segundo veces para la tercera y cuarta prueba. ¿Por qué tienes tiempos tan altos? pastebin.com/qabNMCHS
bad_keypoints
@ronnieaka: Él está recibiendo sub 1 segundo veces para todas las pruebas. Él está obteniendo> 1 µs para el 3 ° y 4 °, lo cual no obtuvo. También obtengo tiempos más lentos en esas pruebas (en Python 2.7.5, Linux). Podría ser CPU, versión, indicadores de compilación, quién sabe.
Thanatos
Estos resultados de referencia son inútiles. Especialmente, el primer caso, que no realiza ninguna concatenación de cadenas, solo devuelve el segundo valor de cadena intacto.
Antti Haapala
3

Un año después, probemos la respuesta de mkoistinen con python 3.4.3:

  • más 0.963564149000 (95.83% tan rápido)
  • unirse 0.923408469000 (100.00% tan rápido)
  • forma 1.501130934000 (61.51% tan rápido)
  • intp 1.019677452000 (90.56% tan rápido)

Nada ha cambiado. Unirse sigue siendo el método más rápido. Sin embargo, dado que intp es posiblemente la mejor opción en términos de legibilidad, es posible que desee utilizar intp.

ramsch
fuente
1
Tal vez podría ser una adición a la respuesta mkoistinen, ya que es un poco corto de una respuesta completa (o al menos agrega el código que está utilizando).
Trilarion
1

Inspirado por los puntos de referencia de @ JasonBaker, aquí hay uno simple que compara 10 "abcdefghijklmnopqrstuvxyz"cadenas, que muestra que .join()es más rápido; incluso con este pequeño aumento en las variables:

Cadena

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

Unirse

>>> x = timeit.Timer(stmt='"".join(["abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz", "abcdefghijklmnopqrstuvxyz"])')
>>> x.timeit()
0.6114138159765048
A
fuente
Eche un vistazo a la respuesta aceptada (desplácese hacia abajo) de esta pregunta: stackoverflow.com/questions/1349311/…
mshsayem
1

Para un pequeño conjunto de cadenas cortas (es decir, 2 o 3 cadenas de no más de unos pocos caracteres), más aún es mucho más rápido. Usando el maravilloso script de mkoistinen en Python 2 y 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)

Entonces, cuando su código está haciendo una gran cantidad de pequeñas concatenaciones separadas, más es la forma preferida si la velocidad es crucial.

usuario7505681
fuente
1

Probablemente "nuevas cadenas f en Python 3.6" es la forma más eficiente de concatenar cadenas.

Usando% s

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

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

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

SuperNova
fuente