¿Cuál es la forma preferida de concatenar una cadena en Python?

358

Dado que Python stringno se puede cambiar, me preguntaba cómo concatenar una cadena de manera más eficiente.

Puedo escribir así:

s += stringfromelsewhere

o así:

s = []
s.append(somestring)

later

s = ''.join(s)

Mientras escribía esta pregunta, encontré un buen artículo hablando sobre el tema.

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

Pero está en Python 2.x., entonces, ¿la pregunta sería si algo cambió en Python 3?

Max
fuente

Respuestas:

434

La mejor manera de agregar una cadena a una variable de cadena es usar +o +=. Esto se debe a que es legible y rápido. También son igual de rápidos, el que elijas es cuestión de gustos, el último es el más común. Aquí hay tiempos con el timeitmódulo:

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

Sin embargo, aquellos que recomiendan tener listas y agregarlas y luego unirse a esas listas, lo hacen porque agregar una cadena a una lista es presumiblemente muy rápido en comparación con extender una cadena. Y esto puede ser cierto, en algunos casos. Aquí, por ejemplo, hay un millón de anexos de una cadena de un carácter, primero a una cadena, luego a una lista:

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

OK, resulta que incluso cuando la cadena resultante tiene un millón de caracteres, la adición fue aún más rápida.

Ahora intentemos agregar una cadena larga de mil caracteres cien mil veces:

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

La cadena final, por lo tanto, termina teniendo aproximadamente 100 MB de longitud. Eso fue bastante lento, agregar a una lista fue mucho más rápido. Que ese momento no incluye la final a.join(). Entonces, ¿cuánto tiempo tomaría eso?

a.join(a):
0.43739795684814453

Oups Resulta que incluso en este caso, agregar / unir es más lento.

Entonces, ¿de dónde viene esta recomendación? Python 2?

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

Bueno, agregar / unir es marginalmente más rápido allí si está usando cadenas extremadamente largas (que generalmente no es así, ¿qué tendría una cadena que tiene 100 MB de memoria?)

Pero el factor decisivo real es Python 2.3. Donde ni siquiera te mostraré los tiempos, porque es tan lento que aún no ha terminado. Estas pruebas de repente toman minutos . Excepto por el append / join, que es tan rápido como en Python posteriores.

Sip. La concatenación de cuerdas fue muy lenta en Python en la edad de piedra. Pero en 2.4 ya no está (o al menos Python 2.4.7), por lo que la recomendación de usar append / join quedó obsoleta en 2008, cuando Python 2.3 dejó de actualizarse, y debería haber dejado de usarlo. :-)

(Actualización: Resulta que cuando hice las pruebas con más cuidado que usar +y también +=es más rápido para dos cadenas en Python 2.3. La recomendación de uso ''.join()debe ser un malentendido)

Sin embargo, esto es CPython. Otras implementaciones pueden tener otras preocupaciones. Y esta es solo otra razón por la cual la optimización prematura es la raíz de todo mal. No use una técnica que se supone "más rápida" a menos que primero la mida.

Por lo tanto, la "mejor" versión para hacer la concatenación de cadenas es usar + o + = . Y si eso resulta lento para usted, lo cual es bastante improbable, haga otra cosa.

Entonces, ¿por qué uso mucho agregar / unir en mi código? Porque a veces es más claro. Especialmente cuando lo que deba concatenar juntos debe estar separado por espacios o comas o líneas nuevas.

Lennart Regebro
fuente
10
Si tiene varias cadenas (n> 10) "" .join (list_of_strings) es aún más rápido
Mikko Ohtamaa
11
la razón por la que + = es rápido es que hay un truco de rendimiento en cpython si el recuento es 1: se desmorona en casi todas las demás implementaciones de python (con la excepción de una construcción de pypy configurada bastante especial)
Ronny
17
¿Por qué se está votando tanto? ¿Cómo es mejor usar un algoritmo que solo sea eficiente en una implementación específica y que tenga lo que esencialmente equivale a un truco frágil para arreglar un algoritmo de tiempo cuadrático? Además, malinterpretas por completo el punto de "la optimización prematura es la raíz de todo mal". Esa cita habla de pequeñas optimizaciones. Esto va de O (n ^ 2) a O (n) que NO es una pequeña optimización.
Wes
12
Aquí está la cita real: "Deberíamos olvidarnos de las pequeñas eficiencias, digamos alrededor del 97% del tiempo: la optimización prematura es la raíz de todo mal. Sin embargo, no debemos dejar pasar nuestras oportunidades en ese crítico 3%. Un buen programador no lo hará". si se deja llevar por la complacencia por tal razonamiento, será prudente mirar cuidadosamente el código crítico; pero solo después de que se haya identificado ese código "
Wes,
2
Nadie dice que a + b sea lento. Es cuadrático cuando estás haciendo a = a + b más de una vez. a + b + c no es lento, repito no lento ya que solo tiene que atravesar cada cadena una vez, mientras que tiene que volver a recorrer las cadenas anteriores muchas veces con el enfoque a = a + b (suponiendo que esté en un bucle De algún tipo). Recuerde que las cadenas son inmutables.
Wes
52

Si está concatenando muchos valores, entonces ninguno. Agregar una lista es costoso. Puedes usar StringIO para eso. Especialmente si lo está acumulando en muchas operaciones.

from cStringIO import StringIO
# python3:  from io import StringIO

buf = StringIO()

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

buf.getvalue()
# 'foofoofoo'

Si ya ha recibido una lista completa de alguna otra operación, simplemente use el ''.join(aList)

De las preguntas frecuentes de Python: ¿Cuál es la forma más eficiente de concatenar muchas cadenas juntas?

Los objetos str y bytes son inmutables, por lo tanto, concatenar muchas cadenas juntas es ineficiente ya que cada concatenación crea un nuevo objeto. En el caso general, el costo total de tiempo de ejecución es cuadrático en la longitud total de la cadena.

Para acumular muchos objetos str, el idioma recomendado es colocarlos en una lista y llamar a str.join () al final:

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

(Otra expresión razonablemente eficiente es usar io.StringIO)

Para acumular muchos objetos de bytes, el idioma recomendado es extender un objeto bytearray usando la concatenación in situ (el operador + =):

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

Editar: Fui tonto y pegué los resultados al revés, por lo que parecía que agregar a una lista era más rápido que cStringIO. También he agregado pruebas para bytearray / str concat, así como una segunda ronda de pruebas usando una lista más grande con cadenas más grandes. (Python 2.7.3)

ejemplo de prueba de ipython para grandes listas de cadenas

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
fuente
2
cStringIOno existe en Py3. Usar en su io.StringIOlugar.
lvc
2
En cuanto a por qué agregar una cadena repetidamente puede ser costoso: joelonsoftware.com/articles/fog0000000319.html
Wes
36

En Python> = 3.6, la nueva cadena f es una forma eficiente de concatenar una cadena.

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

El método recomendado es utilizar append y join.

MRAB
fuente
1
Como puede ver en mi respuesta, esto depende de cuántas cadenas esté concatenando. He hecho algunos tiempos sobre esto (vea la charla que he vinculado en mis comentarios sobre mi respuesta) y, en general, a menos que sean más de diez, use +.
Lennart Regebro
1
PEP8 menciona esto ( python.org/dev/peps/pep-0008/#programming-recommendations ). Lo racional es que mientras CPython tiene optimizaciones especiales para la concatenación de cadenas con + =, otras implementaciones pueden no tenerlo.
Quantum7
8

Si las cadenas que está concatenando son literales, use la concatenación literal de cadenas

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

Esto es útil si desea comentar parte de una cadena (como arriba) o si desea usar cadenas sin procesar o comillas triples para parte de un literal, pero no para todo.

Como esto sucede en la capa de sintaxis, utiliza operadores de concatenación cero.

droide
fuente
7

Escribes esta función

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

Entonces puedes llamar simplemente a donde quieras

str_join('Pine')  # Returns : Pine
str_join('Pine', 'apple')  # Returns : Pineapple
str_join('Pine', 'apple', 3)  # Returns : Pineapple3
Shameem
fuente
1
str_join = lambda *str_list: ''.join(s for s in str_list)
Rick apoya a Mónica el
7

El uso del método de concatenación de cadenas en lugar de '+' es EL PEOR método de concatenación en términos de estabilidad e implementación cruzada, ya que no admite todos los valores. Estándar PEP8 desalienta esto y alienta el uso de format (), join () y append () para uso a largo plazo.

Como se cita en la sección vinculada "Recomendaciones de programación":

Por ejemplo, no confíe en la implementación eficiente de CPython de la concatenación de cadenas en el lugar para las declaraciones en la forma a + = b o a = a + b. Esta optimización es frágil incluso en CPython (solo funciona para algunos tipos) y no está presente en absoluto en implementaciones que no utilizan el recuento. En las partes sensibles de rendimiento de la biblioteca, se debe usar el formulario '' .join () en su lugar. Esto asegurará que la concatenación ocurra en tiempo lineal en varias implementaciones.

badslacks
fuente
55
El enlace de referencia hubiera sido bueno :)
6

Aunque algo anticuado, Code Like a Pythonista: Idiomatic Python recomienda join()más de+ en esta sección . Al igual que PythonSpeedPerformanceTips en su sección sobre concatenación de cadenas , con el siguiente descargo de responsabilidad:

La precisión de esta sección se discute con respecto a las versiones posteriores de Python. En CPython 2.5, la concatenación de cadenas es bastante rápida, aunque esto puede no aplicarse igualmente a otras implementaciones de Python. Ver ConcatenationTestCode para una discusión.

Levon
fuente
6

Como @jdi menciona, la documentación de Python sugiere usar str.joino io.StringIOpara la concatenación de cadenas. Y dice que un desarrollador debe esperar un tiempo cuadrático +=en un bucle, a pesar de que hay una optimización desde Python 2.4. Como dice esta respuesta:

Si Python detecta que el argumento izquierdo no tiene otras referencias, llama reallocpara intentar evitar una copia cambiando el tamaño de la cadena en su lugar. Esto no es algo en lo que deba confiar, porque es un detalle de implementación y porque si realloctermina necesitando mover la cadena con frecuencia, el rendimiento se degrada a O (n ^ 2) de todos modos.

Mostraré un ejemplo de código del mundo real que ingenuamente se basó en +=esta optimización, pero no se aplicó. El siguiente código convierte un iterativo de cadenas cortas en fragmentos más grandes para usar en una API masiva.

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

Este código puede ejecutarse literariamente durante horas debido a la complejidad del tiempo cuadrático. A continuación se presentan alternativas con estructuras de datos sugeridas:

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

Y un micro-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()

micro-benchmark

saaj
fuente
5

Puedes hacerlo de diferentes maneras.

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

Creé este pequeño resumen a través de los siguientes artículos.

Kushan Gunasekera
fuente
3

mi caso de uso fue ligeramente diferente. Tuve que construir una consulta donde más de 20 campos eran dinámicos. Seguí este enfoque de usar el método de formato

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

esto fue comparativamente más simple para mí en lugar de usar + u otras formas

Ishwar Rimal
fuente