Versión corta: si s es una cadena, entonces s = s + 'c' podría modificar la cadena en su lugar, mientras que t = s + 'c' no. Pero, ¿cómo sabe la operación s + 'c' qué escenario se encuentra?
Versión larga:
t = s + 'c' necesita crear una cadena separada porque el programa luego quiere tanto la cadena anterior como s como la nueva cadena como t .
s = s + 'c' puede modificar la cadena en su lugar si s es la única referencia, ya que el programa solo quiere que s sea la cadena extendida. CPython realmente hace esta optimización, si hay espacio al final para el carácter extra.
Considere estas funciones, que agregan un carácter repetidamente:
def fast(n): s = '' for _ in range(n): s = s + 'c' t = s del t def slow(n): s = '' for _ in range(n): t = s + 'c' s = t del t Resultados de referencia con n = 100_000 (¡ Pruébelo en línea! ):
fast : 9 ms 9 ms 9 ms 9 ms 10 ms slow : 924 ms 927 ms 931 ms 933 ms 945 ms Tenga en cuenta que el extra t = s o s = t hace que ambas variables sean referencias equivalentes a la cadena y luego del t deja solo s , por lo que en la siguiente iteración del bucle, s vuelve a ser la única referencia a la cadena. Por lo tanto, la única diferencia entre las dos funciones es el orden en que s + 'c' se asigna a s t .
Desmontemos también el código de bytes. Marqué las únicas tres diferencias con != en el medio. Como era de esperar, solo difieren las variables para STORE_FAST y LOAD_FAST . Pero hasta BINARY_ADD inclusive, el código de bytes es idéntico. Entonces, ¿cómo sabe BINARY_ADD si optimizar o no?
import dis import dis dis.dis(fast) dis.dis(slow) --------------------------------------------------------------------------- 0 LOAD_CONST 1 ('') 0 LOAD_CONST 1 ('') 2 STORE_FAST 1 (s) 2 STORE_FAST 1 (s) 4 LOAD_GLOBAL 0 (range) 4 LOAD_GLOBAL 0 (range) 6 LOAD_FAST 0 (n) 6 LOAD_FAST 0 (n) 8 CALL_FUNCTION 1 8 CALL_FUNCTION 1 10 GET_ITER 10 GET_ITER >> 12 FOR_ITER 18 (to 32) >> 12 FOR_ITER 18 (to 32) 14 STORE_FAST 2 (_) 14 STORE_FAST 2 (_) 16 LOAD_FAST 1 (s) 16 LOAD_FAST 1 (s) 18 LOAD_CONST 2 ('c') 18 LOAD_CONST 2 ('c') 20 BINARY_ADD 20 BINARY_ADD 22 STORE_FAST 1 (s) != 22 STORE_FAST 3 (t) 24 LOAD_FAST 1 (s) != 24 LOAD_FAST 3 (t) 26 STORE_FAST 3 (t) != 26 STORE_FAST 1 (s) 28 DELETE_FAST 3 (t) 28 DELETE_FAST 3 (t) 30 JUMP_ABSOLUTE 12 30 JUMP_ABSOLUTE 12 >> 32 LOAD_CONST 0 (None) >> 32 LOAD_CONST 0 (None) 34 RETURN_VALUE 34 RETURN_VALUEAquí está el código en cuestión , de la rama de Python 3.10 (en ceval.c , y llamado desde la implementación del mismo archivo del código de operación BINARY_ADD ). Como señaló @jasonharper en un comentario, mira hacia adelante para ver si el resultado de BINARY_ADD se vinculará a continuación con el mismo nombre del que proviene el sumando de la izquierda. En fast() , es (el operando proviene de s y el resultado se almacena en s ), pero en slow() no lo es (el operando proviene de s pero se almacena en t ).
Sin embargo, no hay garantía de que esta optimización persista. Por ejemplo, noté que su fast() no es más rápido que su slow() en la rama main de CPython de desarrollo actual (que es el trabajo en progreso actual hacia una versión final 3.11).
Como se señaló, no hay garantía de que esta optimización persistirá. Los programadores de Python "serios" deberían saber que no deben confiar en trucos dudosos específicos de CPython y, de hecho, PEP 8 advierte explícitamente contra confiar en este específico:
El código debe escribirse de manera que no perjudique a otras implementaciones de Python (PyPy, Jython, IronPython, Cython, Psyco, etc.).
Por ejemplo, no confíe en la implementación eficiente de CPython de la concatenación de cadenas en el lugar para declaraciones en la forma
a += boa = a + b...