Concatenación de cadenas frente a sustitución de cadenas en Python


98

En Python, el dónde y cuándo usar la concatenación de cadenas frente a la sustitución de cadenas se me escapa. Dado que la concatenación de cuerdas ha experimentado grandes aumentos en el rendimiento, ¿es esta (cada vez más) una decisión estilística en lugar de práctica?

Para un ejemplo concreto, ¿cómo se debe manejar la construcción de URI flexibles?

DOMAIN = 'http://stackoverflow.com'
QUESTIONS = '/questions'

def so_question_uri_sub(q_num):
    return "%s%s/%d" % (DOMAIN, QUESTIONS, q_num)

def so_question_uri_cat(q_num):
    return DOMAIN + QUESTIONS + '/' + str(q_num)

Editar: También ha habido sugerencias sobre cómo unir una lista de cadenas y usar la sustitución con nombre. Estas son variantes del tema central, que es, ¿de qué manera es la forma correcta de hacerlo y en qué momento? ¡Gracias por las respuestas!


Es curioso, en Ruby, la interpolación de cadenas es generalmente más rápida que la concatenación ...
Keltia

olvidaste volver "" .join ([DOMAIN, QUESTIONS, str (q_num)])
Jimmy

No soy un experto en Ruby, pero apuesto a que la interpolación es más rápida porque las cadenas son mutables en Ruby. Las cadenas son secuencias inmutables en Python.
gotgenes

1
solo un pequeño comentario sobre las URI. Los URI no son exactamente como cadenas. Hay URI, por lo que debe tener mucho cuidado al concatenarlos o compararlos. Ejemplo: un servidor que entrega sus representaciones a través de http en el puerto 80. example.org (sin slah al final) example.org/ (barra) example.org:80/ (slah + puerto 80) son el mismo uri pero no el mismo cuerda.
karlcow

Respuestas:


55

La concatenación es (significativamente) más rápida según mi máquina. Pero estilísticamente, estoy dispuesto a pagar el precio de la sustitución si el rendimiento no es crítico. Bueno, y si necesito formatear, no hay necesidad de hacer la pregunta ... no hay otra opción que usar interpolación / plantilla.

>>> import timeit
>>> def so_q_sub(n):
...  return "%s%s/%d" % (DOMAIN, QUESTIONS, n)
...
>>> so_q_sub(1000)
'http://stackoverflow.com/questions/1000'
>>> def so_q_cat(n):
...  return DOMAIN + QUESTIONS + '/' + str(n)
...
>>> so_q_cat(1000)
'http://stackoverflow.com/questions/1000'
>>> t1 = timeit.Timer('so_q_sub(1000)','from __main__ import so_q_sub')
>>> t2 = timeit.Timer('so_q_cat(1000)','from __main__ import so_q_cat')
>>> t1.timeit(number=10000000)
12.166618871951641
>>> t2.timeit(number=10000000)
5.7813972166853773
>>> t1.timeit(number=1)
1.103492206766532e-05
>>> t2.timeit(number=1)
8.5206360154188587e-06

>>> def so_q_tmp(n):
...  return "{d}{q}/{n}".format(d=DOMAIN,q=QUESTIONS,n=n)
...
>>> so_q_tmp(1000)
'http://stackoverflow.com/questions/1000'
>>> t3= timeit.Timer('so_q_tmp(1000)','from __main__ import so_q_tmp')
>>> t3.timeit(number=10000000)
14.564135316080637

>>> def so_q_join(n):
...  return ''.join([DOMAIN,QUESTIONS,'/',str(n)])
...
>>> so_q_join(1000)
'http://stackoverflow.com/questions/1000'
>>> t4= timeit.Timer('so_q_join(1000)','from __main__ import so_q_join')
>>> t4.timeit(number=10000000)
9.4431309007150048

10
¿Hiciste pruebas con cadenas realmente grandes (como 100000 caracteres)?
drnk

24

No se olvide de la sustitución con nombre:

def so_question_uri_namedsub(q_num):
    return "%(domain)s%(questions)s/%(q_num)d" % locals()

4
Este código tiene al menos 2 malas prácticas de programación: expectativa de variables globales (el dominio y las preguntas no se declaran dentro de la función) y pasar más variables de las necesarias a una función format (). Votar en contra porque esta respuesta enseña malas prácticas de codificación.
jperelli

12

¡Tenga cuidado con la concatenación de cadenas en un bucle! El costo de la concatenación de cadenas es proporcional a la longitud del resultado. El bucle te lleva directamente a la tierra de N-squared. Algunos lenguajes optimizarán la concatenación a la cadena asignada más recientemente, pero es arriesgado contar con el compilador para optimizar su algoritmo cuadrático hasta lineal. Es mejor usar la primitiva ( join?) Que toma una lista completa de cadenas, hace una sola asignación y las concatena todas de una vez.


16
Eso no es actual. En las últimas versiones de Python, se crea un búfer de cadenas oculto cuando concatenas cadenas en un bucle.
Seun Osewa

5
@Seun: Sí, como dije, algunos lenguajes se optimizarán, pero es una práctica arriesgada.
Norman Ramsey

11

"Como la concatenación de cuerdas ha experimentado grandes aumentos en el rendimiento ..."

Si el rendimiento importa, es bueno saberlo.

Sin embargo, los problemas de rendimiento que he visto nunca se han reducido a operaciones con cadenas. En general, me he metido en problemas con las operaciones de E / S, clasificación y O ( n 2 ) que son los cuellos de botella.

Hasta que las operaciones de cadena sean los limitadores del rendimiento, me quedaré con las cosas que son obvias. Principalmente, eso es sustitución cuando es una línea o menos, concatenación cuando tiene sentido y una herramienta de plantilla (como Mako) cuando es grande.


10

Lo que desea concatenar / interpolar y cómo desea formatear el resultado debe impulsar su decisión.

  • La interpolación de cadenas le permite agregar formato fácilmente. De hecho, su versión de interpolación de cadenas no hace lo mismo que su versión de concatenación; en realidad, agrega una barra diagonal adicional antes del q_numparámetro. Para hacer lo mismo, tendría que escribir return DOMAIN + QUESTIONS + "/" + str(q_num)ese ejemplo.

  • La interpolación facilita el formateo de números; "%d of %d (%2.2f%%)" % (current, total, total/current)sería mucho menos legible en forma de concatenación.

  • La concatenación es útil cuando no tiene un número fijo de elementos para encadenar.

Además, sepa que Python 2.6 presenta una nueva versión de interpolación de cadenas, llamada plantilla de cadenas :

def so_question_uri_template(q_num):
    return "{domain}/{questions}/{num}".format(domain=DOMAIN,
                                               questions=QUESTIONS,
                                               num=q_num)

La plantilla de cadenas está programada para reemplazar eventualmente la interpolación porcentual, pero eso no sucederá durante bastante tiempo, creo.


Bueno, sucederá cada vez que decida pasar a Python 3.0. Además, vea el comentario de Peter sobre el hecho de que puede hacer sustituciones con nombre con el operador% de todos modos.
John Fouhy

"La concatenación es útil cuando no tiene un número fijo de elementos para encadenar". - ¿Te refieres a una lista / matriz? En ese caso, ¿no podrías unirte a ellos?
extraño

"¿No podrías unirte a ellos?" - Sí (suponiendo que desee separadores uniformes entre elementos). Las comprensiones de listas y generadores funcionan muy bien con string.join.
Tim Lesher

1
"Bueno, sucederá cada vez que decida pasar a Python 3.0" - No, py3k aún admite el operador%. El siguiente punto de depreciación posible es 3.1, por lo que todavía tiene algo de vida.
Tim Lesher

2
2 años después ... Python 3.2 está a punto de lanzarse y la interpolación de% style todavía está bien.
Corey Goldberg

8

Solo estaba probando la velocidad de diferentes métodos de concatenación / sustitución de cadenas por curiosidad. Una búsqueda en Google sobre el tema me trajo aquí. Pensé que publicaría los resultados de mi prueba con la esperanza de que pudiera ayudar a alguien a decidir.

    import timeit
    def percent_():
            return "test %s, with number %s" % (1,2)

    def format_():
            return "test {}, with number {}".format(1,2)

    def format2_():
            return "test {1}, with number {0}".format(2,1)

    def concat_():
            return "test " + str(1) + ", with number " + str(2)

    def dotimers(func_list):
            # runs a single test for all functions in the list
            for func in func_list:
                    tmr = timeit.Timer(func)
                    res = tmr.timeit()
                    print "test " + func.func_name + ": " + str(res)

    def runtests(func_list, runs=5):
            # runs multiple tests for all functions in the list
            for i in range(runs):
                    print "----------- TEST #" + str(i + 1)
                    dotimers(func_list)

... Después de ejecutar runtests((percent_, format_, format2_, concat_), runs=5), descubrí que el método% era aproximadamente el doble de rápido que los demás en estas pequeñas cadenas. El método concat siempre fue el más lento (apenas). Hubo diferencias muy pequeñas al cambiar las posiciones en elformat() método, pero el cambio de posiciones siempre fue al menos 0.01 más lento que el método de formato normal.

Muestra de resultados de la prueba:

    test concat_()  : 0.62  (0.61 to 0.63)
    test format_()  : 0.56  (consistently 0.56)
    test format2_() : 0.58  (0.57 to 0.59)
    test percent_() : 0.34  (0.33 to 0.35)

Los ejecuté porque utilizo la concatenación de cadenas en mis scripts, y me preguntaba cuál era el costo. Los ejecuté en diferentes órdenes para asegurarme de que nada interfiriera, o que obtuviera un mejor rendimiento siendo el primero o el último. En una nota al margen, agregué algunos generadores de cadenas más largas en esas funciones como "%s" + ("a" * 1024)y el concat regular fue casi 3 veces más rápido (1.1 vs 2.8) que usar los métodos formaty %. Supongo que depende de las cadenas y de lo que intentas lograr. Si el rendimiento realmente importa, sería mejor probar cosas diferentes y probarlas. Tiendo a elegir la legibilidad sobre la velocidad, a menos que la velocidad se convierta en un problema, pero soy solo yo. Así que no me gustó mi copiar / pegar, tuve que poner 8 espacios en todo para que se vea bien. Yo suelo usar 4.


1
Debería considerar seriamente qué está perfilando cómo. Por un lado, su concat es lento porque tiene dos strcasts. Con las cadenas el resultado es el opuesto, ya que la cadena concat es en realidad más rápida que todas las alternativas cuando solo se trata de tres cadenas.
Justus Wingert

@JustusWingert, esto ya tiene dos años. He aprendido mucho desde que publiqué esta 'prueba'. Honestamente, en estos días uso str.format()y str.join()sobre la concatenación normal. También estoy atento a las 'f-strings' de PEP 498 , que ha sido aceptada recientemente. En cuanto a las str()llamadas que afectan el rendimiento, estoy seguro de que tiene razón. No tenía idea de lo caras que eran las llamadas a funciones en ese momento. Sigo pensando que las pruebas deben hacerse cuando haya alguna duda.
Cj Welborn

Después de una prueba rápida con join_(): return ''.join(["test ", str(1), ", with number ", str(2)]), parece joinque también es más lento que el porcentaje.
gaborous

4

Recuerde, las decisiones estilísticas son prácticas, si alguna vez planea mantener o depurar su código :-) Hay una cita famosa de Knuth (¿posiblemente citando a Hoare?): "Deberíamos olvidarnos de las pequeñas eficiencias, digamos aproximadamente el 97% del tiempo: La optimización temprana es la raíz de todo mal."

Siempre que tenga cuidado de no (decir) convertir una tarea O (n) en una tarea O (n 2 ), elegiría la que le resulte más fácil de entender.


0

Utilizo la sustitución siempre que puedo. Solo uso la concatenación si estoy construyendo una cadena en, digamos, un bucle for.


7
"construir una cadena en un bucle for": a menudo, este es un caso en el que puede usar '' .join y una expresión generadora ..
John Fouhy

-1

En realidad, lo correcto, en este caso (construir caminos) es usar os.path.join. Sin concatenación o interpolación de cadenas


1
eso es cierto para las rutas del sistema operativo (como en su sistema de archivos) pero no cuando se construye un URI como en este ejemplo. Los URI siempre tienen '/' como separador.
Andre Blum
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.