Módulo `copy` de Python

Módulo `copy` de Python

Este artículo explica el módulo copy de Python.

Centrándonos en las diferencias entre copias superficiales y profundas, ofrecemos una explicación clara —desde los mecanismos básicos de duplicación de objetos hasta aplicaciones en clases personalizadas— junto con ejemplos prácticos.

YouTube Video

Módulo copy de Python

El módulo copy de Python es el módulo estándar para gestionar la duplicación (copia) de objetos. El objetivo es comprender la diferencia entre copias superficiales y profundas y poder controlar el comportamiento de copiado para objetos personalizados.

Conceptos básicos: ¿Qué es una copia superficial?

Aquí ilustramos el comportamiento de las copias superficiales en objetos mutables como listas y diccionarios. Una copia superficial duplica solo el objeto de nivel superior y comparte las referencias internas.

 1# Example: shallow copy with lists and dicts
 2import copy
 3
 4# A nested list
 5original = [1, [2, 3], 4]
 6shallow = copy.copy(original)
 7
 8# Mutate nested element
 9shallow[1].append(99)
10
11print("original:", original)  # nested change visible in original
12print("shallow: ", shallow)

En este código, la lista de nivel superior se duplica, pero las sublistas internas se comparten por referencia, por lo que shallow[1].append(99) también se refleja en original. Este es un comportamiento típico de una copia superficial.

¿Qué es una copia profunda?

Una copia profunda duplica recursivamente el objeto y todas sus referencias internas, produciendo un objeto independiente. Úsala cuando quieras duplicar de forma segura estructuras complejas y anidadas.

 1# Example: deep copy with nested structures
 2import copy
 3
 4original = [1, [2, 3], {'a': [4, 5]}]
 5deep = copy.deepcopy(original)
 6
 7# Mutate nested element of the deep copy
 8deep[1].append(100)
 9deep[2]['a'].append(200)
10
11print("original:", original)  # original remains unchanged
12print("deep:    ", deep)

En este ejemplo, los cambios en deep no afectan a original. deepcopy copia todo el grafo de objetos, obteniendo una réplica independiente.

Cómo decidir cuál usar

La elección de qué copia usar debe determinarse por la estructura del objeto y tu propósito. Si no quieres que los cambios en los elementos mutables internos afecten al objeto original, deepcopy es apropiado. Esto se debe a que duplica recursivamente todo el objeto, creando una copia completamente independiente.

Por otro lado, si es suficiente duplicar solo el objeto de nivel superior y valoras la velocidad y la eficiencia de memoria, copy (una copia superficial) es más adecuado.

  • Quieres evitar que el original cambie al modificar elementos mutables internos → usa deepcopy.
  • Duplicar solo el nivel superior es suficiente y quieres priorizar el rendimiento (velocidad/memoria) → usa copy (copia superficial).

Manejo de objetos integrados e inmutables

Los objetos inmutables (p. ej., int, str y tuplas cuyo contenido es inmutable) por lo general no necesitan copiarse. copy.copy puede devolver el mismo objeto cuando corresponde.

 1# Example: copying immutables
 2import copy
 3
 4a = 42
 5b = copy.copy(a)
 6print(a is b)  # True for small ints (implementation detail)
 7
 8s = "hello"
 9t = copy.deepcopy(s)
10print(s is t)  # True (strings are immutable)

Dado que copiar objetos inmutables aporta poca ventaja de eficiencia, se puede reutilizar el mismo objeto. Rara vez es algo de lo que debas preocuparte en el diseño de aplicaciones.

Clases personalizadas: definición de __copy__ y __deepcopy__

Si la copia predeterminada no es lo que esperas para tu clase, puedes proporcionar tu propia lógica de copiado. Si defines __copy__ y __deepcopy__, copy.copy y copy.deepcopy los usarán.

 1# Example: customizing copy behavior
 2import copy
 3
 4class Node:
 5    def __init__(self, value, children=None):
 6        self.value = value
 7        self.children = children or []
 8
 9    def __repr__(self):
10        return f"Node({self.value!r}, children={self.children!r})"
11
12    def __copy__(self):
13        # Shallow copy: create new Node, but reuse children list reference
14        new = self.__class__(self.value, self.children)
15        return new
16
17    def __deepcopy__(self, memo):
18        # Deep copy: copy value and deepcopy each child
19        new_children = [copy.deepcopy(child, memo) for child in self.children]
20        new = self.__class__(copy.deepcopy(self.value, memo), new_children)
21        memo[id(self)] = new
22        return new
  • Al implementar __copy__ y __deepcopy__, puedes controlar de forma flexible el comportamiento de copiado específico de la clase. Por ejemplo, puedes manejar casos en los que ciertos atributos deban referirse al mismo objeto (compartirlo), mientras que otros atributos deban duplicarse como objetos completamente nuevos.
  • El argumento memo que se pasa a __deepcopy__ es un diccionario que registra los objetos ya procesados durante la copia recursiva y evita bucles infinitos causados por referencias circulares.
 1# Build tree
 2root_node = Node('root', [Node('child1'), Node('child2')])
 3
 4shallow_copy = copy.copy(root_node)   # shallow copy
 5deep_copy = copy.deepcopy(root_node)  # deep copy
 6
 7# Modify children
 8# affects both root_node and shallow_copy
 9shallow_copy.children.append(Node('child_shallow'))
10# affects deep_copy only
11deep_copy.children.append(Node('child_deep'))
12
13# Print results
14print("root_node:", root_node)
15print("shallow_copy:", shallow_copy)
16print("deep_copy:", deep_copy)
  • En este código, la variable shallow_copy se crea mediante una 'copia superficial', por lo que solo se duplica el nivel superior del objeto, y los objetos a los que hace referencia internamente (en este caso, la lista children) se comparten con el root_node original. Como resultado, cuando agregas un nuevo nodo a shallow_copy, la lista children compartida se actualiza y el cambio también se refleja en el contenido de root_node.
  • Por otro lado, la variable deep_copy se crea mediante una 'copia profunda', por lo que la estructura interna se duplica de forma recursiva, produciendo un árbol completamente independiente de root_node. Por lo tanto, incluso si agregas un nuevo nodo a deep_copy, no afecta a root_node.

Referencias cíclicas (objetos recursivos) y la importancia de memo

Cuando un grafo de objetos complejo se referencia a sí mismo (referencias cíclicas), deepcopy usa un diccionario memo para rastrear los objetos ya copiados y evitar bucles infinitos.

 1# Example: recursive list and deepcopy memo demonstration
 2import copy
 3
 4a = []
 5b = [a]
 6a.append(b)  # a -> [b], b -> [a]  (cycle)
 7
 8# deepcopy can handle cycles
 9deep = copy.deepcopy(a)
10print("deep copy succeeded, length:", len(deep))
  • Internamente, deepcopy usa memo para referirse a objetos ya duplicados, lo que permite un copiado seguro incluso en presencia de ciclos. Necesitas un mecanismo similar cuando realizas recursión manualmente.

copyreg y copiado personalizado al estilo de serialización (avanzado)

Si deseas registrar un comportamiento especial de copiado en coordinación con bibliotecas, puedes usar el módulo estándar copyreg para registrar cómo se (re)construyen los objetos. Esto es útil para objetos complejos y tipos de extensiones en C.

 1# Example: using copyreg to customize pickling/copy behavior (brief example)
 2import copy
 3import copyreg
 4
 5class Wrapper:
 6    def __init__(self, data):
 7        self.data = data
 8
 9def reduce_wrapper(obj):
10    # Return callable and args so object can be reconstructed
11    return (Wrapper, (obj.data,))
12
13copyreg.pickle(Wrapper, reduce_wrapper)
14
15w = Wrapper([1, 2, 3])
16w_copy = copy.deepcopy(w)
17print("w_copy.data:", w_copy.data)
  • Con copyreg, puedes proporcionar reglas de reconstrucción que afecten tanto a pickle como a deepcopy. Es una API avanzada y, en la mayoría de los casos, __deepcopy__ es suficiente.

Precauciones y escollos prácticos

Hay varios puntos importantes para garantizar un comportamiento correcto al usar el módulo copy. A continuación explicamos escollos comunes que puedes encontrar en el desarrollo y cómo evitarlos.

  • Rendimiento deepcopy puede consumir mucha memoria y CPU, así que considera cuidadosamente si es necesario.
  • Manejo de elementos que deseas compartir Si quieres que algunos atributos, como cachés grandes, permanezcan compartidos, evita intencionalmente copiar sus referencias dentro de __deepcopy__.
  • Estado interno inmutable Si mantienes datos inmutables internamente, copiar puede ser innecesario.
  • Hilos y recursos externos Los recursos que no se pueden copiar, como sockets y descriptores de archivos, o bien no tiene sentido copiarlos o provocarán errores, por lo que debes evitar copiarlos por diseño.

Ejemplo práctico: un patrón para actualizar diccionarios de forma segura

Al actualizar un objeto de configuración complejo, este ejemplo usa deepcopy para proteger la configuración original.

 1# Example: safely update a nested configuration using deepcopy
 2import copy
 3
 4default_config = {
 5    "db": {"host": "localhost", "ports": [5432]},
 6    "features": {"use_cache": True, "cache_sizes": [128, 256]},
 7}
 8
 9# Create a working copy to modify without touching defaults
10working = copy.deepcopy(default_config)
11working["db"]["ports"].append(5433)
12working["features"]["cache_sizes"][0] = 512
13
14print("default_config:", default_config)
15print("working:", working)

Con deepcopy, puedes crear de forma segura configuraciones derivadas sin arriesgarte a dañar la configuración predeterminada. Esto es especialmente útil para estructuras mutables anidadas, como las configuraciones.

Mejores prácticas

Para usar el módulo copy de forma segura y eficaz, es importante tener en cuenta las siguientes pautas prácticas.

  • Elige entre copias superficiales y profundas según si habrá cambios y su alcance.
  • Implementa __copy__ y __deepcopy__ en clases personalizadas para lograr el comportamiento esperado.
  • Como las copias profundas son costosas, reduce la necesidad de copiar mediante el diseño cuando sea posible. Considera la inmutabilidad y métodos de clonación explícitos, entre otras técnicas.
  • Al tratar con referencias cíclicas, aprovecha deepcopy o proporciona manualmente un mecanismo similar a memo.
  • Diseña de manera que los recursos externos, como los manejadores de archivos y los hilos, no se copien.

Conclusión

El módulo copy es la herramienta estándar para la duplicación de objetos en Python. Al comprender correctamente las diferencias entre copias superficiales y profundas e implementar un comportamiento de copiado personalizado cuando sea necesario, puedes realizar duplicaciones de forma segura y predecible. Aclarando en la etapa de diseño si la copia es realmente necesaria y qué debe compartirse, puedes evitar errores innecesarios y problemas de rendimiento.

Puedes seguir el artículo anterior utilizando Visual Studio Code en nuestro canal de YouTube. Por favor, también revisa nuestro canal de YouTube.

YouTube Video