Propagación de referencias en objetos mutables: la trampa de la copia
Al finalizar este tema
Comprenderá por qué al copiar una lista en Python mediante = el original se modifica, y podrá distinguir entre copia superficial y copia profunda para utilizarlas correctamente.
Copié, pero el original cambió
original = [1, 2, 3]copy = original
copy.append(4)print(original) # [1, 2, 3, 4] — ¡El original también cambia!copy = original no es una copia. Es simplemente añadir otra etiqueta a la misma lista. original y copy son dos nombres que apuntan a la misma lista.
En el tema anterior (Variables y memoria), aprendimos que una variable no es una "caja que contiene valores", sino una "flecha que apunta a un objeto". Este es precisamente el momento en el que esto puede causar problemas en la práctica.
print(id(original)) # 4392957504print(id(copy)) # 4392957504 — ¡La misma dirección!Copia real — Slicing
original = [1, 2, 3]copy = original[:] # Rebanado completo = nueva lista
copy.append(4)print(original) # [1, 2, 3] — El original no cambiaprint(copy) # [1, 2, 3, 4][:] corta la lista desde el principio hasta el final para crear una nueva lista. list(original) y original.copy() tienen el mismo efecto.
Limitaciones de la copia superficial
matrix = [[1, 2], [3, 4]]shallow = matrix[:]
shallow[0][0] = 99print(matrix) # [[99, 2], [3, 4]] — ¡El original también cambia!El slicing copia solo el primer nivel. matrix[:] crea una nueva lista externa, pero los elementos internos [1, 2] y [3, 4] comparten el mismo objeto. Si se modifica el interior, los cambios también se reflejarán en el original.
Esto es una copia superficial (shallow copy).
Copia profunda — copy.deepcopy
import copy
matrix = [[1, 2], [3, 4]]deep = copy.deepcopy(matrix)
deep[0][0] = 99print(matrix) # [[1, 2], [3, 4]] — El original no cambiadeepcopy realiza una copia recursiva de todos los objetos internos. Es un nuevo objeto completamente independiente.
Cuándo usar cada uno
| Situación | Método | Razón |
|---|---|---|
| Listas unidimensionales | lst[:] o lst.copy() | Rápido y sencillo |
| Listas anidadas (2D o más) | copy.deepcopy(lst) | Requiere independencia de las listas internas |
| Diccionarios | dict.copy() o {**d} | Copia superficial de un solo nivel |
| Diccionarios anidados | copy.deepcopy(d) | Independencia de dicts/list internos |
| Cuando no es necesario copiar | = tal cual | Para compartir intencionalmente el mismo objeto |
Lo mismo se aplica a los argumentos de función
def add_item(lst, item): lst.append(item) return lst
my_list = [1, 2, 3]result = add_item(my_list, 4)
print(my_list) # [1, 2, 3, 4] — El original cambiaAl pasar una lista a una función, se pasa por referencia. Si se .append() dentro de la función, el original también cambiará. Para preservar el original, realice primero una copia dentro de la función:
def add_item_safe(lst, item): new_lst = lst[:] new_lst.append(item) return new_lstResumen de mutabilidad vs. inmutabilidad
| Tipo | Mutable/Inmutable | Efecto en el original al modificar la copia = |
|---|---|---|
int, float, str, tuple | Inmutable | Sin efecto (se crea un nuevo objeto de todos modos) |
list, dict, set | Mutable | El original también cambia |
Los objetos inmutables no presentan este problema, ya que al "modificar" una cadena, siempre se crea una nueva. Solo es necesario tener precaución con los objetos mutables.
Puntos clave
=no es una copia, sino asignar un nuevo nombre al mismo objeto. Para estructuras unidimensionales, use[:]o.copy(); para estructuras anidadas, usecopy.deepcopy(). Si pasa un objeto mutable a una función, el original puede cambiar; si no es su intención, realice una copia primero.