Saltar al contenido
Editor

Copy y deepcopy en Python: por qué tu "copia" no es una copia

Intermedio20 min5 ejercicios70 XP

Esta lección es una traducción del original en inglés.Read in EnglishAvisar de un error de traducción

0/5 ejercicios

Imagina que tienes una carpeta llena de documentos. Fotocopias la carpeta y se la das a un amigo. Pero hay una trampa: las fotos que hay dentro de la carpeta no son copias. Son las mismas fotos de papel. Si tu amigo le dibuja un bigote a una, tu foto original también queda arruinada.

Así es exactamente como funciona la copia por defecto en Python. Crees que has hecho una copia independiente, pero los objetos internos siguen compartidos. Es uno de los errores más confusos de Python, y hace tropezar por igual a principiantes y a programadores con experiencia.

En este tutorial aprenderás la diferencia entre asignación, copia superficial (shallow copy) y copia profunda (deep copy). Al terminar, sabrás exactamente cuándo usar cada una y no volverás a caer en un error de "referencia compartida".

¿Por qué cambiar una "copia" cambia el original?

Empecemos por la sorpresa más común. Cuando escribes b = a, NO estás haciendo una copia. Estás creando un segundo nombre para el mismo objeto. Tanto a como b apuntan a los mismos datos en memoria.

La asignación crea un alias, no una copia
a = [1, 2, 3]
b = a # NOT a copy - just a second name
b.append(4)
print(a) # [1, 2, 3, 4] - a changed too!
print(b) # [1, 2, 3, 4]
Salida
[1, 2, 3, 4]
[1, 2, 3, 4]

Esto pasa porque a y b son dos etiquetas pegadas en la misma caja. Cuando metes algo nuevo en la caja usando la etiqueta b, también lo ves cuando miras por la etiqueta a.

Puedes comprobar que dos variables apuntan al mismo objeto con el operador is o revisando su id().

Comprobar la identidad de un objeto con is e id()
a = [1, 2, 3]
b = a
print(a is b) # True - same object
print(id(a)) # Same number...
print(id(b)) # ...as this one
Salida
True
80036304
80036304

¿Qué es una copia superficial?

Una copia superficial crea un contenedor exterior nuevo, pero NO copia los objetos que hay dentro. La lista nueva es independiente en el nivel superior, pero cualquier objeto anidado sigue compartido con el original.

Hay varias formas de hacer una copia superficial de una lista.

Tres formas de hacer una copia superficial de una lista
original = [1, 2, 3]
# Three ways to shallow copy a list
copy1 = original.copy() # .copy() method
copy2 = original[:] # slice from start to end
copy3 = list(original) # list() constructor
# They are all independent copies
copy1.append(99)
print(original) # [1, 2, 3] - unchanged!
print(copy1) # [1, 2, 3, 99]
Salida
[1, 2, 3]
[1, 2, 3, 99]

Para los diccionarios, usa .copy() o dict().

Copia superficial de un diccionario
original = {'name': 'Alice', 'age': 25}
copy1 = original.copy() # .copy() method
copy2 = dict(original) # dict() constructor
copy1['age'] = 30
print(original) # {'name': 'Alice', 'age': 25} - unchanged
print(copy1) # {'name': 'Alice', 'age': 30}
Salida
{'name': 'Alice', 'age': 25}
{'name': 'Alice', 'age': 30}

Esto parece perfecto. Pero aquí está la trampa: la copia superficial solo copia un nivel. Si tu lista contiene otras listas (o diccionarios), esos objetos internos siguen compartidos.

La trampa de la copia superficial con datos anidados
# Shallow copy with nested data - the trap!
original = [[1, 2], [3, 4], [5, 6]]
shallow = original.copy()
# Modifying the outer list is fine
shallow.append([7, 8])
print(len(original)) # 3 - unaffected
print(len(shallow)) # 4 - only shallow changed
# But modifying an INNER list affects both!
shallow[0].append(99)
print(original[0]) # [1, 2, 99] - original changed too!
print(shallow[0]) # [1, 2, 99]
Salida
3
4
[1, 2, 99]
[1, 2, 99]
Copia superficial: listas internas compartidas
original = [[1, 2], [3, 4]]
shallow = original.copy()
shallow[0].append(99)
print(original)  # [[1, 2, 99], [3, 4]]
Copia profunda: todo independiente
import copy
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
deep[0].append(99)
print(original)  # [[1, 2], [3, 4]]

¿Qué es una copia profunda y cuándo la necesitas?

Una copia profunda crea un clon completamente independiente. Copia el objeto exterior Y copia de forma recursiva cada objeto que hay dentro, sin importar cuántos niveles de anidación tenga. El original y la copia no comparten nada.

Para hacer una copia profunda, necesitas el módulo copy de la biblioteca estándar de Python.

La copia profunda crea un clon totalmente independiente
import copy
original = [[1, 2], [3, 4], [5, 6]]
deep = copy.deepcopy(original)
# Modify the deep copy
deep[0].append(99)
deep.append([7, 8])
print(original) # [[1, 2], [3, 4], [5, 6]] - completely unchanged!
print(deep) # [[1, 2, 99], [3, 4], [5, 6], [7, 8]]
Salida
[[1, 2], [3, 4], [5, 6]]
[[1, 2, 99], [3, 4], [5, 6], [7, 8]]

La copia profunda también funciona con diccionarios, incluidas las estructuras con muchos niveles de anidación.

Copia profunda con diccionarios y listas anidados
import copy
user = {
'name': 'Alice',
'scores': [90, 85, 92],
'profile': {'level': 5, 'badges': ['star', 'gold']},
}
clone = copy.deepcopy(user)
# Modify the clone at every level
clone['name'] = 'Bob'
clone['scores'].append(100)
clone['profile']['badges'].append('diamond')
print(user['name']) # Alice
print(user['scores']) # [90, 85, 92]
print(user['profile']['badges']) # ['star', 'gold']
print(clone['name']) # Bob
print(clone['scores']) # [90, 85, 92, 100]
print(clone['profile']['badges']) # ['star', 'gold', 'diamond']
Salida
Alice
[90, 85, 92]
['star', 'gold']
Bob
[90, 85, 92, 100]
['star', 'gold', 'diamond']

Por dentro: cómo funcionan las referencias en Python

Para entender de verdad las copias, necesitas saber cómo guarda Python los datos. Cada dato vive en algún lugar de la memoria como un objeto. Las variables son solo nombres que hacen referencia a esos objetos (apuntan a ellos).

La asignación copia la referencia. La copia superficial crea un contenedor nuevo con las mismas referencias dentro. La copia profunda crea un contenedor nuevo con referencias nuevas que apuntan a copias nuevas de todo lo que hay dentro.

Visualizar referencias con is
import copy
inner = [1, 2, 3]
original = [inner, inner] # Both slots point to the same inner list
shallow = copy.copy(original)
deep = copy.deepcopy(original)
print(original[0] is original[1]) # True - same inner list
print(shallow[0] is original[0]) # True - shallow shares inner objects
print(deep[0] is original[0]) # False - deep made a new copy
Salida
True
True
False

Aquí tienes un resumen de los tres enfoques en forma de tabla.

Resumen: asignación, copia superficial y copia profunda
# Summary:
# ─────────────────────────────────────────────────────
# Method | New outer? | New inner? | Safe for nested?
# ─────────────────────────────────────────────────────
# b = a | No | No | No
# b = a.copy() | Yes | No | No
# b = deepcopy(a) | Yes | Yes | Yes
# ─────────────────────────────────────────────────────
print("Assignment: same outer, same inner")
print("Shallow copy: new outer, same inner")
print("Deep copy: new outer, new inner")
Salida
Assignment:   same outer, same inner
Shallow copy: new outer, same inner
Deep copy:    new outer, new inner

¿Cuándo usar cada tipo de copia?

Usa la asignación (b = a) cuando quieras a propósito dos nombres para los mismos datos. Es habitual al pasar datos a funciones: normalmente quieres que la función trabaje con el original.

Usa una copia superficial cuando tengas una lista o un diccionario plano (sin objetos mutables anidados) y necesites una copia independiente. Esto cubre la mayoría de los casos del día a día.

Usa una copia profunda cuando tengas objetos mutables anidados (listas de listas, diccionarios con listas como valores, etc.) y necesites un clon totalmente independiente. Es imprescindible cuando quieres modificar la copia sin ningún riesgo de afectar al original.


Ejercicios de práctica

Predice la salida: alias por asignación
Predice la salida

Lee el código con atención. ¿Qué va a imprimir? Recuerda que = con listas crea un alias, no una copia.

Escribe la salida exacta.

a = [10, 20, 30]
b = a
b[1] = 99
print(a)
print(a is b)
Haz una copia independiente
Corrige el error

El código de abajo intenta crear una copia de seguridad de la lista original antes de modificarla. ¡Pero la copia de seguridad también cambia! Corrige el código para que la copia de seguridad no cambie.

Salida esperada:

Original: [1, 2, 3, 4]
Backup: [1, 2, 3]
original = [1, 2, 3]
backup = original
original.append(4)
print(f"Original: {original}")
print(f"Backup: {backup}")
Predice la salida: la trampa de la copia superficial
Predice la salida

Este código usa una copia superficial con datos anidados. Léelo con atención y predice qué se va a imprimir.

Escribe la salida exacta.

original = [[1, 2], [3, 4]]
shallow = original.copy()
shallow[0].append(5)
print(original)
print(shallow)
Copia segura de datos anidados
Escribe el código

Crea una copia totalmente independiente de los datos de original usando una copia profunda. Luego añade la puntuación 100 a la lista de puntuaciones de la copia. Imprime las puntuaciones del original y las de la copia en líneas separadas para demostrar que son independientes.

original = {
'name': 'Alice',
'scores': [85, 90, 78],
}
# Create a deep copy
# Add 100 to the copy's scores
# Print original scores, then copy's scores
Corrige el error del estado del juego
Corrige el error

Este juego guarda un punto de control con el estado del jugador y luego el jugador recoge un objeto. ¡Pero cuando cargamos el punto de control, el objeto ya está ahí! Corrige el código para que el punto de control sea una instantánea realmente independiente.

Salida esperada:

Checkpoint inventory: ['sword', 'shield']
Current inventory: ['sword', 'shield', 'potion']
player = {
'name': 'Hero',
'hp': 100,
'inventory': ['sword', 'shield'],
}
# Save checkpoint
checkpoint = player.copy()
# Player picks up a potion
player['inventory'].append('potion')
print(f"Checkpoint inventory: {checkpoint['inventory']}")
print(f"Current inventory: {player['inventory']}")

Tutoriales relacionados

¿Te sirvió este tutorial?

Sugerir una mejora