La cadena de Python ‘unirse’ es más rápida (?) Que ‘+’, pero ¿qué ocurre aquí?

Pregunté el método más eficiente para la concatenación de cadenas dinámica masiva en una publicación anterior y se me sugirió usar el método de unión , el mejor, el más simple y el más rápido para hacerlo (como todos dijeron). Pero mientras jugaba con concatenaciones de cuerdas, encontré algunos resultados extraños (?). Estoy seguro de que algo está pasando pero no puedo entenderlo. Aquí esta lo que hice:

Definí estas funciones:

import timeit def x(): s=[] for i in range(100): # Other codes here... s.append("abcdefg"[i%7]) return ''.join(s) def y(): s='' for i in range(100): # Other codes here... s+="abcdefg"[i%7] return s def z(): s='' for i in range(100): # Other codes here... s=s+"abcdefg"[i%7] return s def p(): s=[] for i in range(100): # Other codes here... s+="abcdefg"[i%7] return ''.join(s) def q(): s=[] for i in range(100): # Other codes here... s = s + ["abcdefg"[i%7]] return ''.join(s) 

He tratado de mantener otras cosas (excepto la concatenación) casi iguales en todas las funciones. Luego probé con lo siguiente con los resultados en el comentario (usando Python 3.1.1 IDLE en una máquina de Windows de 32 bits):

 timeit.timeit(x) # 31.54912480500002 timeit.timeit(y) # 23.533029429999942 timeit.timeit(z) # 22.116181330000018 timeit.timeit(p) # 37.718607439999914 timeit.timeit(q) # 108.60377576499991 

Eso significa que muestra que strng = strng + dyn_strng es el más rápido. Aunque la diferencia en los tiempos no es tan significativa (excepto la última), quiero saber por qué sucede esto. ¿Es porque estoy usando Python 3.1.1 y eso proporciona ‘+’ como más eficiente? ¿Debo usar ‘+’ como alternativa para unirme ? O, ¿he hecho algo extremadamente tonto? ¿O que? Por favor explique claramente.

Algunos de nosotros, los comentaristas de Python, creo que en su mayoría Rigo y Hettinger, hicieron todo lo posible (en el camino a 2.5, creo) para optimizar algunos casos especiales de la muy s += something muy común s += something desastrosa , argumentando que se demostró que los principiantes nunca estarán convencidos de que ''.join es el camino correcto y la horrible lentitud del += podría estar dando un mal nombre a Python. Otros de nosotros no estábamos tan entusiasmados, porque simplemente no podían optimizar cada ocurrencia (o incluso la mayoría de ellos) para lograr un desempeño decente; pero no nos sentimos lo suficientemente calientes como para tratar de bloquearlos activamente.

Creo que este hilo prueba que deberíamos habernos opuesto a ellos más severamente. Tal como está ahora, optimizaron += en un cierto subconjunto de casos difícil de predecir hasta donde puede ser un 20% más rápido para casos estúpidos en particular que la forma correcta (que sigue siendo ''.join ) – solo una forma perfecta de atrapar a los principiantes para que persigan esas ganancias irrelevantes del 20% utilizando el lenguaje incorrecto … al costo, de vez en cuando y de su POV de la nada, de ser golpeado con una pérdida de rendimiento del 200% (o más) , ya que el comportamiento no lineal todavía está al acecho justo afuera de las esquinas que Hettinger y Rigo prepararon y pusieron flores 😉 – una que IMPORTA, una que las hará miserables. Esto va en contra del grano de Python “idealmente, solo una manera obvia de hacerlo” y me parece que, colectivamente, hemos tendido una trampa para principiantes, la mejor clase, también … los que no solo aceptan lo que dicen sus “mejores”, pero curiosamente preguntan y exploran.

Ah, bueno, me rindo. OP, @mshsayem, adelante, use + = en todas partes, disfrute de sus incrementos de velocidad irrelevantes del 20% en casos triviales, minúsculos e irrelevantes, y será mejor que los disfrute al máximo – porque un día, cuando no pueda verlos próximamente, en una operación GRANDE IMPORTANTE, el camión con remolque que se aproxima le golpeará en el diafragma con una desaceleración del 200% (a menos que tenga mala suerte y sea uno de 2000% ;-). Solo recuerda: si alguna vez sientes que “Python es terriblemente lento”, RECUERDA, lo más probable es que sea uno de tus bucles amados de += dar la vuelta y morder la mano que lo alimenta.

Para el rest de nosotros, aquellos que entienden lo que significa decir que debemos olvidarnos de las pequeñas eficiencias, digamos que aproximadamente el 97% del tiempo , seguiré recomendando con entusiasmo ''.join , para que todos podamos dormir con toda tranquilidad y tranquilidad. SABEMOS que no nos veremos afectados por una desaceleración superlineal cuando menos lo esperemos y menos podamos costearlo. Pero para ti, Armin Rigo y Raymond Hettinger (los dos últimos, queridos amigos personales míos, BTW, no solo compañeros de compromiso ;-): ¡que tu += sea ​​suave y que tu gran O nunca sea peor que N! )

Entonces, para el rest de nosotros, aquí hay un conjunto de mediciones más significativo e interesante:

 $ python -mtimeit -s'r=[str(x)*99 for x in xrange(100,1000)]' 's="".join(r)' 1000 loops, best of 3: 319 usec per loop 

900 cuerdas de 297 caracteres cada una, sumrse a la lista directamente es, por supuesto, más rápida, pero el OP está aterrorizado por tener que hacer apéndices antes de esa fecha. Pero:

 $ python -mtimeit -s'r=[str(x)*99 for x in xrange(100,1000)]' 's=""' 'for x in r: s+=x' 1000 loops, best of 3: 779 usec per loop $ python -mtimeit -s'r=[str(x)*99 for x in xrange(100,1000)]' 'z=[]' 'for x in r: z.append(x)' '"".join(z)' 1000 loops, best of 3: 538 usec per loop 

… con una cantidad de datos semi-importante (unos pocos .append de KB – tomando una fracción mensurable de milisegundos en todas las direcciones), incluso el bien antiguo antiguo .append es superior. Además, es obvio y trivialmente fácil de optimizar:

 $ python -mtimeit -s'r=[str(x)*99 for x in xrange(100,1000)]' 'z=[]; zap=z.append' 'for x in r: zap(x)' '"".join(z)' 1000 loops, best of 3: 438 usec per loop 

afeitando otras décimas de milisegundo sobre el tiempo de bucle promedio. Todo el mundo (al menos todo el que está totalmente obsesionado con el rendimiento) obviamente sabe que ALISTE (eliminar el bucle interno por un cálculo repetitivo que de otra manera se realizaría una y otra vez) es una técnica crucial para la optimización: Python no se alza en su nombre. , así que tienes que hacer tu propia elevación en esas raras ocasiones en las que cada microsegundo es importante.

En cuanto a por qué q es mucho más lento: cuando dices

 l += "a" 

estás agregando la cadena "a" al final de l , pero cuando dices

 l = l + ["a"] 

está creando una nueva lista con los contenidos de l y ["a"] y luego reasignando los resultados a l . Así se generan constantemente nuevas listas.

Supongo que x () es más lento porque primero estás creando la matriz y luego uniéndote a ella. Por lo tanto, no solo está midiendo el tiempo que toma la unión, sino también el tiempo que toma construir la matriz.

En un escenario en el que ya tiene una matriz y desea crear una cadena a partir de sus elementos, la unión debería ser más rápida que iterar a través de la matriz y construir la cadena paso a paso.

Esta pregunta es realmente acerca de lo que cuestan las cosas. Jugaremos un poco rápido y suelto aquí, restando resultados en casos similares. Puedes decidir por ti mismo si este es un método válido. Aquí hay algunos casos de prueba básicos:

 import timeit def append_to_list_with_join(): s=[] for i in xrange(100): s.append("abcdefg"[i%7]) return ''.join(s) def append_to_list_with_join_opt(): s=[] x = s.append for i in xrange(100): x("abcdefg"[i%7]) return ''.join(s) def plus_equals_string(): s='' for i in xrange(100): s+="abcdefg"[i%7] return s def plus_assign_string(): s='' for i in xrange(100): s=s+"abcdefg"[i%7] return s def list_comp_join(): return ''.join(["abcdefg"[i%7] for i in xrange(100)]) def list_comp(): return ["abcdefg"[i%7] for i in xrange(100)] def empty_loop(): for i in xrange(100): pass def loop_mod(): for i in xrange(100): a = "abcdefg"[i%7] def fast_list_join(): return "".join(["0"] * 100) for f in [append_to_list_with_join, append_to_list_with_join_opt, plus_equals_string,plus_assign_string,list_comp_join, list_comp, empty_loop,loop_mod, fast_list_join]: print f.func_name, timeit.timeit(f) 

Y aquí está lo que cuestan:

 append_to_list_with_join 25.4540209021 append_to_list_with_join_opt 19.9999782794 plus_equals_string 16.7842428996 plus_assign_string 14.8312124167 list_comp_join 16.329590353 list_comp 14.6934344309 empty_loop 2.3819276612 loop_mod 10.1424356308 fast_list_join 2.58149394686 

En primer lugar, muchas cosas tienen costos inesperados en Python. append_to_list_with_join versus append_to_list_with_join_opt muestra que incluso buscar un método en un objeto tiene un costo no despreciable. En este caso, mirar hacia arriba es una cuarta parte del tiempo.

A continuación, list_comp_join versus list_comp muestra que join () es bastante rápido: toma alrededor de 1.7 o solo el 10% del tiempo de list_comp_join.

loop_mod muestra que la mayor parte de esta prueba es en realidad para configurar los datos, independientemente del método de construcción de la cadena. Por inferencia, el tiempo necesario para “string = string +”, “string + =” y la comprensión de la lista son:

 plus_equals_string = 16.78 - 10.14 = 6.64 plus_assign_string = 14.83 - 10.14 = 4.69 list_comp = 14.69 - 10.14 = 4.55 

En cuanto a la pregunta del OP, join () es rápido, pero el tiempo para crear la lista subyacente, ya sea con primitivas de lista o comprensión de lista, es comparable a crear la cadena con primitivos de cadena. Si ya tiene una lista, conviértala en una cadena con join () – será rápida.

Los tiempos que presenta el OP indican que la construcción de listas utilizando operadores de concatenación es lenta. En contraste, el uso de listas de comprensión es rápido. Si tiene que construir una lista, use una lista de comprensión.

Finalmente, tomemos tres de las funciones más cercanas del OP: ¿cuál es la diferencia entre x, p y q? Vamos a simplificar un poco:

 import timeit def x(): s=[] for i in range(100): s.append("c") def p(): s=[] for i in range(100): s += "c" def q(): s=[] for i in range(100): s = s + ["c"] for f in [x,p,q]: print f.func_name, timeit.timeit(f) 

Aquí están los resultados:

 x 16.0757342064 p 87.1533697719 q 85.0999698984 

Y aquí está el desassembly :

 >>> import dis >>> dis.dis(x) 2 0 BUILD_LIST 0 3 STORE_FAST 0 (s) 3 6 SETUP_LOOP 33 (to 42) 9 LOAD_GLOBAL 0 (range) 12 LOAD_CONST 1 (100) 15 CALL_FUNCTION 1 18 GET_ITER >> 19 FOR_ITER 19 (to 41) 22 STORE_FAST 1 (i) 4 25 LOAD_FAST 0 (s) 28 LOAD_ATTR 1 (append) 31 LOAD_CONST 2 ('c') 34 CALL_FUNCTION 1 37 POP_TOP 38 JUMP_ABSOLUTE 19 >> 41 POP_BLOCK >> 42 LOAD_CONST 0 (None) 45 RETURN_VALUE >>> dis.dis(p) 2 0 BUILD_LIST 0 3 STORE_FAST 0 (s) 3 6 SETUP_LOOP 30 (to 39) 9 LOAD_GLOBAL 0 (range) 12 LOAD_CONST 1 (100) 15 CALL_FUNCTION 1 18 GET_ITER >> 19 FOR_ITER 16 (to 38) 22 STORE_FAST 1 (i) 4 25 LOAD_FAST 0 (s) 28 LOAD_CONST 2 ('c') 31 INPLACE_ADD 32 STORE_FAST 0 (s) 35 JUMP_ABSOLUTE 19 >> 38 POP_BLOCK >> 39 LOAD_CONST 0 (None) 42 RETURN_VALUE >>> dis.dis(q) 2 0 BUILD_LIST 0 3 STORE_FAST 0 (s) 3 6 SETUP_LOOP 33 (to 42) 9 LOAD_GLOBAL 0 (range) 12 LOAD_CONST 1 (100) 15 CALL_FUNCTION 1 18 GET_ITER >> 19 FOR_ITER 19 (to 41) 22 STORE_FAST 1 (i) 4 25 LOAD_FAST 0 (s) 28 LOAD_CONST 2 ('c') 31 BUILD_LIST 1 34 BINARY_ADD 35 STORE_FAST 0 (s) 38 JUMP_ABSOLUTE 19 >> 41 POP_BLOCK >> 42 LOAD_CONST 0 (None) 45 RETURN_VALUE 

Los bucles son casi idénticos. La comparación equivale a CALL_FUNCTION + POP_TOP versus INPLACE_ADD + STORE_FAST vs. BUILD_LIST + BINARY_ADD + STORE_FAST. Sin embargo, no puedo dar una explicación de más bajo nivel, simplemente no puedo encontrar los costos de los códigos de byte de python en la red. Sin embargo, puede que te inspires al mirar el módulo Python de la semana de Doug Hellmann, que se publica en dis .

Está midiendo dos operaciones distintas: la creación de un conjunto de cadenas y la concatenación de cadenas.

  import timeit def x(): s = [] for i in range(100): s.append("abcdefg"[i%7]) return ''.join(s) def y(): s = '' for i in range(100): s += "abcdefgh"[i%7] # timeit.timeit(x) returns about 32s # timeit.timeit(y) returns about 23s 

De lo anterior, de hecho parecería que ‘+’ es una operación más rápida que unir. Pero considere:

  src = [] def c(): global src s = [] for i in range(100): s.append("abcdefg"[i%7]) src = s def x2(): return ''.join(src) def y2(): s = '' for i in range(len(src)): s += src[i] return s # timeit.timeit(c) returns about 30s # timeit.timeit(x2) returns about 1.5s # timeit.timeit(y2) returns about 14s 

En otras palabras, al cronometrar x () vs y (), su resultado está contaminado por la construcción de su matriz de origen. Si lo rompes, encuentras que la unión es más rápida.

Además, estás trabajando con arreglos pequeños, y los números de tiempo coinciden. Si aumenta significativamente el tamaño de la matriz y la longitud de cada cadena, las diferencias son más claras:

  def c2(): global src s = [] for i in range(10000): s.append("abcdefghijklmnopqrstuvwxyz0123456789" src = s # timeit.timeit(x2, number=10000) returns about 1s # timeit.timeit(y2, number=10000) returns about 80s 

Hay una diferencia entre + = y + con las cadenas: si no hay otras referencias a “x”, x + = y solo puede agregarse a x, en lugar de tener que tomar una copia de la cadena para agregarla, que es lo mismo beneficio que obtiene al utilizar “” .join ().

El principal beneficio de “” .join () sobre + o + = es que join () siempre debe dar un rendimiento lineal, mientras que en muchos casos + / + = dará un rendimiento cuadrático (es decir, cuando duplica la cantidad de texto, cuadruplicar la cantidad de tiempo empleado). Pero esto solo importará con mucho texto, no solo 100 bytes, y creo que no se activará si solo tiene una referencia a la cadena a la que está agregando.

En detalle:

El mejor rendimiento de su caso para la concatenación de cadenas es mirar cada carácter en la cadena final una vez. “” .join () hace eso de forma natural: tiene toda la información que necesita desde el principio.

Sin embargo, a + = b puede funcionar de dos maneras, simplemente puede agregar “b” a una cadena existente, en cuyo caso solo necesita mirar los caracteres en “b”, o puede tener que mirar los caracteres en ” un “también

En C, strcat () siempre mira a todos los caracteres en ambas cadenas, por lo que siempre funciona mal. Sin embargo, en Python, la longitud de la cadena se almacena, por lo que la cadena se puede extender siempre que no se haga referencia en otro lugar, y se obtiene un buen rendimiento al copiar solo los caracteres en “b”. Si se hace referencia en otro lugar, python hará una copia de “a” primero, luego agregará “b” al final, lo que le dará un mal rendimiento. Si está agregando cinco cadenas de esta manera, el tiempo que tomará será:

 ab = a+b # Time is a + b abc = ab+c # Time is (a+b) + c abcd = abc+d # Time is (a+b+c) + d abcde = abcd+e # Time is (a+b+c+d) + e 

que si a, b, c, d, e son aproximadamente del mismo tamaño, digamos, n, es n * (n-1) / 2-1 operaciones, o esencialmente n cuadrado.

Para obtener el mal comportamiento para x + = y, intente:

 def a(n=100): res = "" for k in xrange(n): v=res res += "foobar" return res 

Aunque v no se usa realmente, es suficiente para desencadenar el camino más lento para + = y obtener el mal comportamiento que preocupa a las personas.

Creo que + = no se introdujo hasta Python 2.0, por lo que no fue posible agregarlo eficientemente sin usar algo como “.join () en Python 1.6 y anteriores.

Ya hay muchos buenos resúmenes aquí, pero solo para más pruebas.

Fuente: ¡Observé el código fuente de Python durante una hora y calculé las complejidades!

Mis hallazgos.

Para 2 cuerdas. (Supongamos que n es la longitud de ambas cadenas)

 Concat (+) - O(n) Join - O(n+k) effectively O(n) Format - O(2n+k) effectively O(n) 

Para más de 2 cuerdas. (Suponga que n es la longitud de todas las cadenas)

 Concat (+) - O(n^2) Join - O(n+k) effectively O(n) Format - O(2n+k) effectively O(n) 

RESULTADO:

Si tiene dos cadenas, técnicamente la concatenación (+) es mejor, aunque en realidad es exactamente lo mismo que unir y formatear.

Si tiene más de dos cadenas, Concat se vuelve horrible y la combinación y el formato son efectivamente iguales, aunque técnicamente la combinación es un poco mejor.

RESUMEN:

Si no te importa la eficiencia, utiliza cualquiera de los anteriores. (Aunque desde que hiciste la pregunta supongo que te importa)

Por lo tanto –

Si tiene 2 cadenas, utilice concat (cuando no esté en un bucle)

Si tiene más de dos cadenas (todas las cadenas) (o en un bucle) use join

Si tienes algo no cadenas utiliza formato, porque duh.

¡Espero que esto ayude!

He descubierto la respuesta a partir de las respuestas publicadas aquí por los expertos. La concatenación de cadenas de Python (y las mediciones de tiempo) depende de estos (hasta donde he visto):

  • Número de concatenaciones
  • Longitud media de las cuerdas
  • Número de llamadas a funciones

He construido un nuevo código que los relaciona. Gracias a Peter S Magnusson, sepp2k, hughdbrown, David Wolever y otros por indicar los puntos importantes que me había perdido antes. Además, en este código podría haber perdido algo. Por lo tanto, aprecio mucho cualquier respuesta que señale nuestros errores, sugerencias, críticas, etc. Después de todo, estoy aquí para aprender. Aquí está mi nuevo código:

 from timeit import timeit noc = 100 tocat = "a" def f_call(): pass def loop_only(): for i in range(noc): pass def concat_method(): s = '' for i in range(noc): s = s + tocat def list_append(): s=[] for i in range(noc): s.append(tocat) ''.join(s) def list_append_opt(): s = [] zap = s.append for i in range(noc): zap(tocat) ''.join(s) def list_comp(): ''.join(tocat for i in range(noc)) def concat_method_buildup(): s='' def list_append_buildup(): s=[] def list_append_opt_buildup(): s=[] zap = s.append def function_time(f): return timeit(f,number=1000)*1000 f_callt = function_time(f_call) def measure(ftuple,n,tc): global noc,tocat noc = n tocat = tc loopt = function_time(loop_only) - f_callt buildup_time = function_time(ftuple[1]) -f_callt if ftuple[1] else 0 total_time = function_time(ftuple[0]) return total_time, total_time - f_callt - buildup_time - loopt*ftuple[2] functions ={'Concat Method\t\t':(concat_method,concat_method_buildup,True), 'List append\t\t\t':(list_append,list_append_buildup,True), 'Optimized list append':(list_append_opt,list_append_opt_buildup,True), 'List comp\t\t\t':(list_comp,0,False)} for i in range(5): print("\n\n%d concatenation\t\t\t\t10'a'\t\t\t\t 100'a'\t\t\t1000'a'"%10**i) print('-'*80) for (f,ft) in functions.items(): print(f,"\t|",end="\t") for j in range(3): t = measure(ft,10**i,'a'*10**j) print("%.3f %.3f |" % t,end="\t") print() 

Y aquí está lo que tengo. [En la columna de tiempo se muestran dos veces (escalada): la primera es el tiempo total de ejecución de la función, y la segunda es la hora real de concatenación (?). He deducido el tiempo de llamada de la función, el tiempo de acumulación de la función (tiempo de inicialización) y el tiempo de iteración. Aquí estoy considerando un caso en el que no se puede hacer sin bucle (diga más enunciado dentro).]

 1 concatenation 1'a' 10'a' 100'a' ------------------- ---------------------- ------------------- ---------------- List comp | 2.310 2.168 | 2.298 2.156 | 2.304 2.162 Optimized list append | 1.069 0.439 | 1.098 0.456 | 1.071 0.413 Concat Method | 0.552 0.034 | 0.541 0.025 | 0.565 0.048 List append | 1.099 0.557 | 1.099 0.552 | 1.094 0.552 10 concatenations 1'a' 10'a' 100'a' ------------------- ---------------------- ------------------- ---------------- List comp | 3.366 3.224 | 3.473 3.331 | 4.058 3.916 Optimized list append | 2.778 2.003 | 2.956 2.186 | 3.417 2.639 Concat Method | 1.602 0.943 | 1.910 1.259 | 3.381 2.724 List append | 3.290 2.612 | 3.378 2.699 | 3.959 3.282 100 concatenations 1'a' 10'a' 100'a' ------------------- ---------------------- ------------------- ---------------- List comp | 15.900 15.758 | 17.086 16.944 | 20.260 20.118 Optimized list append | 15.178 12.585 | 16.203 13.527 | 19.336 16.703 Concat Method | 10.937 8.482 | 25.731 23.263 | 29.390 26.934 List append | 20.515 18.031 | 21.599 19.115 | 24.487 22.003 1000 concatenations 1'a' 10'a' 100'a' ------------------- ---------------------- ------------------- ---------------- List comp | 134.507 134.365 | 143.913 143.771 | 201.062 200.920 Optimized list append | 112.018 77.525 | 121.487 87.419 | 151.063 117.059 Concat Method | 214.329 180.093 | 290.380 256.515 | 324.572 290.720 List append | 167.625 133.619 | 176.241 142.267 | 205.259 171.313 10000 concatenations 1'a' 10'a' 100'a' ------------------- ---------------------- ------------------- ---------------- List comp | 1309.702 1309.560 | 1404.191 1404.049 | 2912.483 2912.341 Optimized list append | 1042.271 668.696 | 1134.404 761.036 | 2628.882 2255.804 Concat Method | 2310.204 1941.096 | 2923.805 2550.803 | STUCK STUCK List append | 1624.795 1251.589 | 1717.501 1345.137 | 3182.347 2809.233 

Para resumir todo esto, he tomado estas decisiones por mí:

  1. Si tiene una lista de cadenas disponible, el método de cadena ‘unirse’ es mejor y más rápido.
  2. Si puedes usar la comprensión de listas, eso también es lo más fácil y rápido.
  3. Si necesita de 1 a 10 concatenación (promedio) con longitud de 1 a 100, agregue la lista, ‘+’ toma el mismo tiempo (casi, tenga en cuenta que los tiempos se escalan).
  4. Añadir lista optimizada parece muy bueno en la mayoría de las situaciones.
  5. Cuando #concatenation o la longitud de la cadena aumenta, ‘+’ comienza a tomar mucho más tiempo. Tenga en cuenta que, para 10000 concatenaciones con 100’a ‘¡mi PC está bloqueada!
  6. Si usa la lista de anexos y ‘unirse’ siempre, estará seguro todo el tiempo (señalado por Alex Martelli).
  7. Pero en alguna situación, por ejemplo, si necesita tomar información del usuario e imprimir “¡Hola mundo de usuarios!”, Es más fácil de usar “+”. Creo que construir una lista y unirme para este caso como x = input (“Ingrese el nombre de usuario:”) y luego x.join ([“¡Hola,”, el mundo de “!”)) Es más feo que “¡El mundo de Hello% s! “% x o” Hola “+ x +” en el mundo ”
  8. Python 3.1 ha mejorado el rendimiento de concatenación. Pero, en alguna implementación como Jython, ‘+’ es menos eficiente.
  9. La optimización prematura es la raíz de todo mal (dicho de los expertos). La mayoría de las veces no necesita optimización. Por lo tanto, no pierda tiempo en la aspiración de optimización (a menos que esté escribiendo un proyecto grande o computacional en el que cada segundo micro / mili cuente).
  10. Use esta información y escríbala de la manera que desee, teniendo en cuenta las circunstancias.
  11. Si realmente necesita optimización, use un generador de perfiles, encuentre los cuellos de botella e intente optimizarlos.

Finalmente, estoy tratando de aprender Python más profundamente. Entonces, no es raro que haya errores (error) en mis observaciones. Entonces, comente sobre esto y sugiérame si estoy tomando una ruta incorrecta. Gracias a todos por participar.

Interesante: he realizado algunas pruebas en las que el tamaño de la cadena cambia, y esto es lo que encontré:

 def x(): x = "a" * 100 s=[] for i in range(100): # Other codes here... s.append(x) return ''.join(s) def z(): x = "a" * 100 s='' for i in xrange(100): # Other codes here... s=s+x return s from timeit import timeit print "x:", timeit(x, number=1000000) print "z:", timeit(z, number=1000000) 

Para cadenas de longitud 1 ( x = "a" * 1 ):

 x: 27.2318270206 z: 14.4046051502 

Para cuerdas de longitud 100:

 x: 30.0796670914 z: 21.5891489983 

Y para cadenas de longitud 1000, se ejecutan 100.000 veces en lugar de 1.000.000.

 x: 14.1769361496 z: 31.4864079952 

Lo cual, si mi lectura de Objects/stringobject.c es correcta, tiene sentido.

Parece, en una primera lectura, que el algoritmo String.join (aparte de los casos) es:

 def join(sep, sequence): size = 0 for string in sequence: size += len(string) + len(sep) result = malloc(size) for string in sequence: copy string into result copy sep into result return result 

Por lo tanto, esto requerirá más o menos pasos O(S) (donde S es la sum de las longitudes de todas las cadenas que se unen).

Además de lo que dijeron los otros, 100 cuerdas de 1 char es realmente pequeño . (Estoy un poco sorprendido de que obtengas una separación de resultados). Ese es el tipo de conjunto de datos que cabe en el caché de tu procesador. No vas a ver un rendimiento asintótico en un microbenchmark.

La concatenación de cadenas era mucho más lenta antes de Python 2.5, cuando aún creaba una nueva copia para cada concatenación de cadenas en lugar de agregarse al original, lo que hacía que join () se convirtiera en una solución popular.

Aquí hay un antiguo punto de referencia que demuestra el viejo problema: http://www.skymind.com/~ocrow/python_string/