Inmutabilidad de las cadenas y reasignación de memoria
Al finalizar este tema
Comprenderás por qué las cadenas en Python son inmutables, cómo funcionan las operaciones de cadenas en la memoria y cómo evitar problemas de rendimiento al procesar grandes volúmenes de cadenas.
Las cadenas son inmutables
name = "Hello"name[0] = "h"# TypeError: 'str' object does not support item assignmentLas cadenas de Python son objetos inmutables. Una vez creadas, su contenido no se puede modificar. Todas las operaciones que parecen una "modificación" en realidad generan una nueva cadena.
name = "Hello"print(id(name)) # 4385123456
name = name.lower()print(id(name)) # 4385123520 ← ¡objeto diferente!id() devuelve la dirección de memoria del objeto. Dado que el id cambió después de lower(), se creó una nueva cadena en lugar de modificar la existente.
Mutable vs. Inmutable
| Tipo | Modificable | Ejemplo |
|---|---|---|
str | Inmutable | "hello" |
tuple | Inmutable | (1, 2, 3) |
int | Inmutable | 42 |
float | Inmutable | 3.14 |
list | Mutable | [1, 2, 3] |
dict | Mutable | {"a": 1} |
set | Mutable | {1, 2, 3} |
# Listas: mutables — el contenido se puede modificar directamenteitems = [1, 2, 3]items[0] = 99print(items) # [99, 2, 3] — mismo objeto, solo cambia el contenido
# Cadenas: inmutables — no se pueden modificar, se crea un nuevo objetotext = "hello"new_text = text.replace("h", "H")print(text) # "hello" — original intactoprint(new_text) # "Hello" — nuevo objeto¿Por qué se diseñó como inmutable?
1. Hashable — Puede usarse como clave de diccionario
# Las cadenas pueden usarse como claves de diccionario (porque son inmutables)scores = {"alice": 90, "bob": 85}
# Las listas no pueden usarse como claves de diccionario (porque son mutables)# scores = {[1, 2]: "value"} # TypeError: unhashable type: 'list'Los diccionarios buscan datos mediante el valor hash de la clave. Si la clave cambia, el valor hash también cambia, lo que impide localizar los datos. Solo los objetos inmutables pueden ser hasheables.
2. Seguridad — se pueden compartir sin riesgo
def greet(name): greeting = "Hello, " + name return greeting
original = "World"result = greet(original)print(original) # "World" — la función no puede modificar originalIncluso si se pasa una cadena a una función, no hay que preocuparse de que el original cambie dentro de la función. Las listas pueden modificarse en su origen dentro de la función .append().
3. Seguridad de hilos
Cuando varios hilos leen la misma cadena simultáneamente, nadie puede modificarla, por lo que no se necesitan bloqueos (locks).
Internamiento de cadenas
Python reutiliza las cadenas que se usan con frecuencia. Esto se denomina internamiento (interning).
a = "hello"b = "hello"print(a is b) # True — ¡comparten el mismo objeto!print(id(a) == id(b)) # True
c = "hello world!"d = "hello world!"print(c is d) # False o True — depende de la implementaciónLas cadenas cortas y sencillas (en formato de identificador) se someten a interning. Aunque una cadena con el mismo contenido aparezca varias veces, solo existe una instancia en la memoria. Esto es posible porque son inmutables: al compartirlas, nadie puede modificarlas.
# is vs ==a = "hello"b = "hello"print(a == b) # True — ¿los valores son iguales?print(a is b) # True — ¿son el mismo objeto? (debido al interning)
# Para comparar cadenas, usa siempre ==. is es impredecibleTrampa de rendimiento: concatenación de cadenas
# Malo: concatenación de cadenas en un bucle — O(n²)result = ""for i in range(10000): result += str(i) + "," # ¡Se crea una nueva cadena cada vez!En cada iteración se crea una nueva cadena y se copia el contenido anterior. Al repetir el proceso 10 000 veces, se producen aproximadamente 50 millones de copias de caracteres (1 + 2 + 3 + ... + 10 000).
# Bueno: usar join — O(n)result = ",".join(str(i) for i in range(10000))join() calcula el tamaño final de antemano y asigna la memoria una sola vez. Esto reduce el número de copias a O(n).
Comparación de rendimiento
import time
# Método 1: += (lento)start = time.time()result = ""for i in range(100000): result += str(i)print(f"+=: {time.time() - start:.3f}s")
# Método 2: join (rápido)start = time.time()result = "".join(str(i) for i in range(100000))print(f"join: {time.time() - start:.3f}s")
# Método 3: io.StringIO (texto masivo)import iostart = time.time()buf = io.StringIO()for i in range(100000): buf.write(str(i))result = buf.getvalue()print(f"StringIO: {time.time() - start:.3f}s")
# Ejemplo de resultado:# +=: 0.852s# join: 0.031s (27 veces más rápido)# StringIO: 0.029sPatrones prácticos
Métodos para "modificar" cadenas de texto
text = "Hello, World!"
# Mayúsculas y minúsculastext.upper() # "HELLO, WORLD!"text.lower() # "hello, world!"text.title() # "Hello, World!"
# Reemplazotext.replace("World", "Python") # "Hello, Python!"
# Modificación por carácter (vía lista)chars = list(text) # ['H', 'e', 'l', 'l', 'o', ...]chars[0] = 'h'result = "".join(chars) # "hello, World!"Todos los métodos devuelven una nueva cadena. El original no cambia.
Combinación de f-strings
name = "Hoon"age = 30greeting = f"Hello, {name}! You are {age} years old."Las f-strings crean la cadena final de una sola vez internamente. Esto es más eficiente y legible que realizar múltiples concatenaciones con +.
Comparación con otros lenguajes
// Java — String es inmutable, StringBuilder es mutable
String s = "Hello";
s = s + " World"; // Se crea un nuevo objeto String
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // Modificación en el mismo objeto (mutable)
String result = sb.toString();// JavaScript — Las cadenas son inmutables (igual que Python)
let s = "Hello";
s[0] = "h"; // No genera error, pero se ignora
console.log(s); // "Hello" (sin cambios)// C — El array de char es mutable
char s[] = "Hello";
s[0] = 'h'; // OK — cambia a "hello"En la mayoría de los lenguajes modernos (Python, Java, JavaScript, Go, C#), las cadenas de texto son inmutables. Esta es una decisión de diseño común para garantizar la seguridad y la optimización. El StringBuilder de Java y el strings.Builder de Go cumplen la misma función que el join() o el io.StringIO de Python: siguen el patrón de ensamblar el contenido en un búfer mutable para luego crear la cadena inmutable final.
Resumen clave
| Concepto | Resumen |
|---|---|
| Inmutable | No se puede modificar tras su creación. Toda "modificación" genera un nuevo objeto. |
id() | Comprobar la dirección de memoria de un objeto. |
| Interning | Las cadenas idénticas se reutilizan en memoria (posible gracias a la inmutabilidad). |
join() | La forma correcta de concatenar cadenas. Usar += en bucles resulta en O(n²). |
is vs == | is comprueba la identidad del objeto, mientras que == comprueba la igualdad de valores. Para cadenas, use ==. |
La inmutabilidad de las cadenas es una decisión de diseño en Python. Sus beneficios —como poder usarse como claves de diccionarios, permitir el paso seguro de datos entre funciones y optimizar la memoria mediante el interning— compensan la restricción de no poder modificarse. Lo único que debe recordar en la práctica es: no acumule cadenas con += dentro de un bucle; use join() en su lugar.