Saltar al contenido
> 💻 🧠 Código 1001 > 📑 Hojas de Trucos > 🐍 Hojas de Trucos de Python > Una guía completa para copiar objetos en Python

Una guía completa para copiar objetos en Python

🔎 Para empezar, ¿por qué es importante la copia de objetos?

En Python, las variables no son «cajas» que contienen datos, sino «etiquetas» o referencias que apuntan a objetos en la memoria. Cuando realizas una asignación simple, como esta:

lista_a = [1, 2, 3]
lista_b = lista_a

No estás creando una nueva lista. Simplemente estás creando otra referencia (lista_b) que apunta exactamente al mismo objeto de lista.

👉 Cualquier cambio en lista_a se reflejará inmediatamente en lista_b (y viceversa) porque son el mismo objeto.

Analogía: Imagina que tienes un documento de Google Docs. Si le envías un enlace a un amigo, él estará editando tu documento original. Eso es una asignación. Sin embargo, si seleccionas «Archivo» → «Hacer una copia», se crea un documento completamente independiente. El módulo copy en Python existe para crear este tipo de copias independientes.

⚙️ Dos tipos de copia

El módulo copy proporciona dos funciones clave:

  • copy.copy() — crea una copia superficial (shallow copy).
  • copy.deepcopy() — crea una copia profunda (deep copy).

La principal diferencia entre ellas es cómo manejan los objetos anidados (como listas dentro de listas o diccionarios dentro de diccionarios).


🟦 Copia superficial (copy.copy())

copy.copy() crea un nuevo objeto contenedor de nivel superior pero, en lugar de copiar los objetos anidados, inserta referencias a ellos en el nuevo contenedor.

import copy

lista_original = [1, 2, [3, 4]]
copia_superficial = copy.copy(lista_original)

# Modifica la lista anidada en el original
lista_original[2][0] = 99

print(f"Original: {lista_original}")    # Salida: Original: [1, 2, [99, 4]]
print(f"Copia superficial: {copia_superficial}") # Salida: Copia superficial: [1, 2, [99, 4]]

Como puedes ver, modificar la lista anidada afectó tanto al original como a la copia porque comparten esa lista anidada.

Diagrama ASCII:

lista_original    → [ 1, 2, ───► lista_interna ]
copia_superficial → [ 1, 2, ───► lista_interna ]
                               lista_interna   → [ 99, 4 ]

🟩 Copia profunda (copy.deepcopy())

copy.deepcopy() resuelve el problema de las referencias compartidas. Esta función recorre recursivamente todo el objeto y crea copias completas e independientes de todo lo que encuentra, incluyendo todos los objetos anidados.

import copy

lista_original = [1, 2, [3, 4]]
copia_profunda = copy.deepcopy(lista_original)

# Modifica la lista anidada en el original
lista_original[2][0] = 99

print(f"Original: {lista_original}") # Salida: Original: [1, 2, [99, 4]]
print(f"Copia profunda: {copia_profunda}")     # Salida: Copia profunda: [1, 2, [3, 4]]

Ahora, la copia_profunda es completamente independiente.

Diagrama ASCII:

lista_original → [ 1, 2, ───► lista_a ]
copia_profunda → [ 1, 2, ───► lista_b ]
                           lista_a   → [ 99, 4 ]
                           lista_b   → [ 3, 4 ]

📑 Tabla de referencia: copy.copy() vs copy.deepcopy()

Tipo de objetoEjemplocopy.copy() (superficial)copy.deepcopy() (profunda)
list (lista)[1, 2, [3, 4]]Una nueva lista, pero la lista anidada [3, 4] es compartidaUna copia completamente independiente; la lista anidada también se copia
dict (diccionario){"a": 1, "b": {"c": 2}}Un nuevo diccionario, pero el {"c": 2} anidado es compartidoUn diccionario independiente y todos los diccionarios anidados
set (conjunto){1, 2, (3, 4)}Un nuevo conjunto; los elementos mutables anidados serían compartidosUn nuevo conjunto; todos los elementos anidados se copian recursivamente
tuple (tupla)(1, 2, [3, 4])Devuelve la misma tupla, pero la lista anidada es compartidaUna nueva tupla con una copia de la lista anidada
str, int, float, bool"hola", 42, 3.14, TrueDevuelve el mismo objeto (son inmutables, no es necesario copiarlos)Lo mismo que la copia superficial
clase personalizadaNodo(1, Nodo(2))Un nuevo objeto de nivel superior; los objetos anidados son compartidosUna copia completamente independiente del objeto y de todos los objetos anidados
objeto con __slots__Una clase con __slots__Solo se copia el objeto de nivel superiorTodos los objetos anidados se copian si admiten la copia
archivo / socket / streamopen("file.txt")Error (objeto no copiable)Error (objeto no copiable)

🧪 Ejemplos prácticos para la tabla

📌 list

import copy
a = [1, 2, [3, 4]]
b = copy.copy(a)
c = copy.deepcopy(a)
a[2][0] = 99
print(f"copy: {b}")      # [1, 2, [99, 4]]
print(f"deepcopy: {c}")  # [1, 2, [3, 4]]

📌 dict

import copy
a = {"a": 1, "b": {"c": 2}}
b = copy.copy(a)
c = copy.deepcopy(a)
a["b"]["c"] = 42
print(f"copy: {b}")      # {'a': 1, 'b': {'c': 42}}
print(f"deepcopy: {c}")  # {'a': 1, 'b': {'c': 2}}

📌 set

import copy
a = {1, 2, (3, 4)} # Los conjuntos solo pueden contener tipos inmutables
b = copy.copy(a)
c = copy.deepcopy(a)
print(b == c) # True (pero son dos objetos independientes en memoria)

📌 tuple

import copy
a = (1, 2, [3, 4]) # La tupla es inmutable, pero su elemento de lista no lo es
b = copy.copy(a)
c = copy.deepcopy(a)
a[2][0] = 99
print(f"copy: {b}")      # (1, 2, [99, 4])
print(f"deepcopy: {c}")  # (1, 2, [3, 4])

📌 Clase personalizada

import copy
class Nodo:
    def __init__(self, valor, hijo=None):
        self.valor = valor
        self.hijo = hijo

a = Nodo(1, Nodo(2))
b = copy.copy(a)
c = copy.deepcopy(a)
a.hijo.valor = 99
print(f"copy: {b.hijo.valor}")      # 99
print(f"deepcopy: {c.hijo.valor}")  # 2

📌 Personalización del comportamiento: __copy__ y __deepcopy__

Puedes controlar cómo se copian tus propios objetos definiendo métodos especiales en su clase.

import copy
class Personalizada:
    def __init__(self, x):
        self.x = x
    def __copy__(self):
        print("¡Se llamó a __copy__!")
        # Devuelve una nueva instancia, pero con la misma referencia a los datos internos
        return Personalizada(self.x)
    def __deepcopy__(self, memo):
        # memo es un diccionario para rastrear objetos ya copiados (previene la recursión)
        print("¡Se llamó a __deepcopy__!")
        # Crea una copia completamente nueva, incluyendo los datos anidados
        return Personalizada(copy.deepcopy(self.x, memo))

obj = Personalizada([1, 2])
c1 = copy.copy(obj)       # Salida: ¡Se llamó a __copy__!
c2 = copy.deepcopy(obj)   # Salida: ¡Se llamó a __deepcopy__!

⚡ Rendimiento y posibles problemas

  • Velocidad: copy.copy() es significativamente más rápido porque no recorre recursivamente toda la estructura de datos. copy.deepcopy() puede ser muy lento en objetos grandes y profundamente anidados.
  • Objetos no copiables: Los recursos del sistema como archivos abiertos, sockets o flujos de datos no se pueden copiar. Intentar hacerlo generará un error.
  • Referencias cíclicas: deepcopy es lo suficientemente inteligente como para manejar referencias cíclicas (cuando un objeto A se refiere a B, y B se refiere de nuevo a A) sin caer en un bucle infinito.

Cuándo usar cada uno: Las reglas principales

  1. Usa una copia superficial (copy.copy()) si tu objeto solo contiene datos inmutables o si intencionadamente quieres que la copia y el original compartan objetos anidados. Es rápido y eficiente.
  2. Usa una copia profunda (copy.deepcopy()) cuando necesites una copia completa y 100% independiente del original, especialmente al trabajar con estructuras de datos complejas (listas, diccionarios, instancias de clase).
  3. Para objetos inmutables (números, cadenas, tuplas sin elementos mutables), la copia generalmente no tiene sentido; Python ya optimiza su uso.
  4. Recuerda que puedes personalizar el proceso con __copy__ y __deepcopy__ para tener un control total.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *