Asignar una lista, un diccionario o una instancia a otra variable no crea automáticamente un objeto nuevo. En Python, la asignación normalmente crea otro nombre que apunta al mismo valor. Por eso, una modificación realizada desde una variable puede aparecer inesperadamente en la otra. El módulo copy en Python ofrece herramientas explícitas para copias superficiales, copias profundas y reemplazo de campos en objetos de estilo inmutable.
Esta guía explica identidad frente a igualdad, copy.copy(), copy.deepcopy(), el diccionario interno memo, la personalización mediante __copy__() y __deepcopy__(), y la función copy.replace(). El contenido complementa los artículos sobre listas en Python, slicing en Python, igualdad e identidad, diccionarios en Python y descriptors en Python.
Asignar no significa copiar
original = [1, 2, 3]
alias = original
alias.append(4)
print(original) # [1, 2, 3, 4]
print(alias is original) # TrueAmbos nombres señalan la misma lista. append() modifica el objeto compartido. El operador is confirma identidad: las dos variables apuntan exactamente al mismo objeto.
Este comportamiento es eficiente y suele ser correcto. Solo necesitas una copia cuando el programa requiere modificaciones independientes.
Copia superficial con copy.copy()
copy.copy() crea un nuevo objeto compuesto, pero reutiliza las referencias a los objetos internos. Esto se denomina copia superficial.
import copy
original = [[1, 2], [3, 4]]
superficial = copy.copy(original)
print(superficial is original) # False
print(superficial[0] is original[0]) # TrueLa lista exterior es nueva, pero las listas interiores siguen compartidas. Añadir una fila nueva a la copia no modifica el origen. Cambiar una fila existente afecta a ambas.
superficial.append([5, 6])
superficial[0].append(99)
print(original) # [[1, 2, 99], [3, 4]]
print(superficial) # [[1, 2, 99], [3, 4], [5, 6]]Métodos nativos de copia
Las listas, diccionarios y conjuntos tienen métodos convenientes:
lista_nueva = lista_antigua.copy()
diccionario_nuevo = diccionario_antiguo.copy()
conjunto_nuevo = conjunto_antiguo.copy()Un slice completo también produce una copia superficial de una lista o secuencia:
nueva = antigua[:]Estas variantes son claras cuando el código conoce el tipo concreto. copy.copy() resulta útil en funciones genéricas e instancias personalizadas. La documentación oficial del módulo copy también indica que algunos métodos nativos y slices pueden devolver una instancia del tipo base al copiar una subclase, mientras copy.copy() normalmente conserva el tipo concreto.
Copia profunda con copy.deepcopy()
copy.deepcopy() recorre recursivamente un objeto compuesto y copia sus componentes. Así evita compartir accidentalmente valores mutables anidados.
import copy
original = {
"usuario": "Ana",
"permisos": ["leer", "editar"],
"preferencias": {"tema": "oscuro"},
}
profunda = copy.deepcopy(original)
profunda["permisos"].append("publicar")
profunda["preferencias"]["tema"] = "claro"
print(original["permisos"])
print(original["preferencias"])La lista y el diccionario internos son independientes, por lo que cambiar la copia no altera el original.
Deepcopy no duplica todo
Una copia profunda no garantiza una identidad nueva para cada valor. Los objetos inmutables, funciones y clases pueden reutilizarse. Los módulos, archivos abiertos, sockets, frames de ejecución, ventanas y recursos similares no se copian de forma genérica.
La copia recursiva también puede duplicar demasiado. Conexiones, locks, servicios compartidos, registros y cachés globales normalmente deben seguir siendo únicos. Usa deepcopy() solo cuando la independencia recursiva forme parte del contrato del objeto.
Referencias recursivas
Un objeto puede apuntar a sí mismo:
elementos = []
elementos.append(elementos)Un copiador recursivo ingenuo entraría en un bucle infinito. deepcopy() evita ese problema mediante un diccionario interno llamado memo, que registra los objetos ya procesados durante la operación.
import copy
elementos = []
elementos.append(elementos)
copia = copy.deepcopy(elementos)
print(copia is copia[0]) # True
print(copia is elementos) # FalseLa autorreferencia se conserva sin reutilizar la lista original.
Por qué memo es importante
El diccionario memo también conserva el uso compartido interno. Si dos rutas del objeto original apuntan al mismo componente, el grafo copiado normalmente hará que ambas rutas apunten a una única copia de ese componente.
import copy
configuracion = {"limite": 10}
original = [configuracion, configuracion]
copia = copy.deepcopy(original)
print(copia[0] is copia[1]) # True
print(copia[0] is configuracion) # FalseEsta propiedad evita duplicaciones inconsistentes y conserva relaciones de identidad importantes.
Una copia profunda puede copiar demasiado
Considera un objeto de dominio que contiene datos y un servicio externo compartido:
class Pedido:
def __init__(self, elementos, pasarela):
self.elementos = elementos
self.pasarela = pasarelaLos elementos pueden necesitar independencia, mientras la pasarela de pago debe seguir compartida. Una copia profunda automática no siempre expresa esa regla. La solución es personalizar el comportamiento.
Personalizar __copy__()
Una clase puede definir __copy__() para controlar su copia superficial:
import copy
class Documento:
def __init__(self, titulo, metadatos):
self.titulo = titulo
self.metadatos = metadatos
def __copy__(self):
return type(self)(self.titulo, self.metadatos)
documento = Documento("Informe", {"version": 1})
nuevo = copy.copy(documento)
print(nuevo is documento)
print(nuevo.metadatos is documento.metadatos)La instancia es nueva, pero los metadatos permanecen compartidos, que es la semántica superficial esperada.
Personalizar __deepcopy__()
__deepcopy__(self, memo) recibe el diccionario de memoización. Debes tratarlo como una estructura opaca y pasarlo a las llamadas recursivas de copy.deepcopy().
import copy
class Pedido:
def __init__(self, elementos, pasarela):
self.elementos = elementos
self.pasarela = pasarela
def __deepcopy__(self, memo):
nuevo = type(self)(
copy.deepcopy(self.elementos, memo),
self.pasarela,
)
memo[id(self)] = nuevo
return nuevoLa colección de elementos se vuelve independiente, mientras el servicio continúa compartido. Registrar la nueva instancia en memo es especialmente importante cuando existen ciclos o referencias repetidas.
Actualizaciones inmutables con copy.replace()
Agregada en Python 3.13, copy.replace() crea un objeto nuevo del mismo tipo reemplazando solamente campos seleccionados. Es una opción práctica para registros de estilo inmutable.
from copy import replace
from dataclasses import dataclass
@dataclass(frozen=True)
class Producto:
nombre: str
precio: float
stock: int
original = Producto("Teclado", 120.0, 15)
promocion = replace(original, precio=99.9)
print(original)
print(promocion)El objeto de origen queda intacto. La función soporta dataclasses, named tuples y clases que implementan __replace__().
copy.replace() no es deepcopy()
replace() sustituye campos concretos y reutiliza los valores restantes. Un campo mutable que no se reemplaza continúa compartido.
from copy import replace
from dataclasses import dataclass
@dataclass(frozen=True)
class Plan:
nombre: str
pasos: list[str]
base = Plan("Lanzamiento", ["prueba", "produccion"])
nuevo = replace(base, nombre="Lanzamiento v2")
print(nuevo.pasos is base.pasos) # TrueUtiliza replace() para actualizaciones funcionales de registros, no como sinónimo de independencia recursiva.
Implementar __replace__()
Las clases personalizadas pueden adoptar el protocolo:
class Coordenada:
def __init__(self, x, y):
self.x = x
self.y = y
def __replace__(self, **changes):
permitidos = {"x", "y"}
desconocidos = set(changes) - permitidos
if desconocidos:
raise TypeError(f"Campos inválidos: {desconocidos}")
return type(self)(
changes.get("x", self.x),
changes.get("y", self.y),
)Después, copy.replace(coordenada, x=10) crea una nueva instancia con una actualización controlada.
Diccionarios anidados y trampas de copia superficial
predeterminado = {
"api": {"timeout": 10},
"logs": {"nivel": "INFO"},
}
produccion = predeterminado.copy()
produccion["api"]["timeout"] = 30
print(predeterminado["api"]["timeout"]) # 30El diccionario exterior se copia, pero los diccionarios internos siguen compartidos. Los árboles de configuración independientes requieren reconstrucción explícita, merge controlado o una copia profunda segura.
Copiar listas de objetos
Copiar una lista no copia las instancias almacenadas:
usuarios_copia = usuarios.copy()
print(usuarios_copia is usuarios) # False
print(usuarios_copia[0] is usuarios[0]) # TrueEsto es útil cuando la nueva lista representa otro orden o una vista filtrada de los mismos objetos. Si cada usuario debe ser independiente, utiliza un método de clonación de dominio o una copia profunda cuidadosamente diseñada.
Cuándo reconstruir de forma explícita
Los modelos de dominio suelen beneficiarse de construir el nuevo valor de forma explícita. El código muestra qué campos se copian, recalculan o comparten intencionalmente.
nuevo_pedido = Pedido(
elementos=[elemento.clonar() for elemento in pedido.elementos],
pasarela=pedido.pasarela,
)Esta estrategia es más extensa, pero puede comunicar la propiedad de los objetos mejor que una copia recursiva genérica.
Rendimiento y memoria
Una copia superficial suele costar tiempo proporcional al número de referencias del contenedor exterior. Una copia profunda recorre el grafo alcanzable y puede consumir mucha más CPU y memoria.
Evita copiar estructuras grandes repetidamente sin medir. Los valores inmutables, el uso compartido estructural, el reemplazo de campos, los generadores y el almacenamiento persistente pueden ser mejores alternativas. El tutorial oficial de estructuras de datos aporta contexto sobre listas, diccionarios, conjuntos y secuencias.
Probar la independencia correcta
No pruebes solamente igualdad. Verifica identidad y mutaciones posteriores:
import copy
original = {"elementos": [[1], [2]]}
copia = copy.deepcopy(original)
assert copia == original
assert copia is not original
assert copia["elementos"] is not original["elementos"]
assert copia["elementos"][0] is not original["elementos"][0]
copia["elementos"][0].append(9)
assert original["elementos"][0] == [1]Las clases personalizadas también deben probarse con ciclos, referencias repetidas, recursos compartidos y subclases.
Errores frecuentes
- Suponer que
b = acrea un objeto nuevo. - Usar
list.copy()esperando duplicar elementos anidados. - Aplicar
deepcopy()sin criterio a sockets, locks, conexiones o servicios. - Destruir relaciones de identidad significativas dentro del grafo.
- Implementar
__deepcopy__()sin reenviarmemo. - Tratar
copy.replace()como una copia profunda. - Duplicar estructuras grandes sin medir el coste.
- Copiar cuando una reconstrucción explícita sería más clara.
Buenas prácticas
- Define primero el nivel de independencia requerido.
- Usa copia superficial cuando solo debe cambiar el contenedor exterior.
- Usa copia profunda cuando todo el estado mutable alcanzable debe ser independiente.
- Personaliza las clases que contienen recursos compartidos.
- Usa
copy.replace()para actualizaciones funcionales de registros compatibles. - Documenta qué componentes siguen compartidos.
- Prueba igualdad, identidad y mutaciones posteriores.
- Mide tiempo y memoria en grafos grandes.
Conclusión
El módulo copy en Python resuelve una distinción fundamental: crear otro nombre, copiar solamente el objeto exterior y duplicar recursivamente un grafo son operaciones diferentes. copy.copy() crea una copia superficial. copy.deepcopy() utiliza memoización para tratar ciclos y conservar el uso compartido interno. copy.replace() crea versiones actualizadas de registros compatibles sustituyendo campos concretos.
La elección correcta depende de la propiedad de los objetos. Las copias superficiales son eficientes y comparten componentes intencionalmente. Las copias profundas aportan independencia, pero pueden duplicar demasiado. Los protocolos personalizados combinan ambas estrategias cuando un objeto contiene responsabilidades mixtas. Hacer explícita esta decisión evita mutaciones inesperadas, consumo innecesario de memoria y duplicación incorrecta de recursos.







