Copiar objetos con el módulo estándar «copy»

Copiar objetos con el módulo estándar «copy»



(Este artículo no trata sobre cómo copiar archivos o carpetas; para ello véase Operaciones con archivos y carpetas).

El módulo estándar copy permite crear copias de distintos objetos de Python, generalmente colecciones mutables (como las listas y los diccionarios) e instancias de clases, también mutables. Este artículo se relaciona con el funcionamiento de los objetos en Python y cómo difieren con el concepto de “variables” en otros lenguajes, para lo cual escribí un artículo hace un tiempo.

Vayamos directamente a un ejemplo. Consideremos la siguiente lista.

>>> a = list(range(5))
>>> a
[0, 1, 2, 3, 4]

Si por el motivo que fuese requiero crear una lista igual a la anterior, intuitivamente haría lo siguiente.

>>> b = a
>>> b
[0, 1, 2, 3, 4]

El problema con esta solución es que no estamos creando dos listas con los mismos elementos, sino que la lista es siempre una y a ella se puede acceder a través de dos nombres diferentes (a y b). Eso se comprueba sencillamente viendo cómo alterando los elementos de un objeto se refleja en el otro.

>>> b.append(5)
>>> a
[0, 1, 2, 3, 4, 5]
>>> del a[2]
>>> b
[0, 1, 3, 4, 5]

Sumado a que el operador is nos confirma que efectivamente se trata del mismo objeto.

>>> a is b
True

Pero en ocasiones realmente queremos crear dos objetos iguales aunque independientes el uno del otro, es decir, cada uno con su espacio asignado en la memoria. Allí es donde nos auxilia la función copy.copy().

>>> import copy
>>> a = list(range(5))
>>> b = copy.copy(a)
>>> a is b
False
>>> b.append(5)
>>> a
[0, 1, 2, 3, 4]
>>> b
[0, 1, 2, 3, 4, 5]

En el caso particular de las listas, este resultado también se obtiene aplicando la operación de slicing.

>>> a = list(range(5))
>>> b = a[:]
>>> a is b
False
>>> a
[0, 1, 2, 3, 4]
>>> b
[0, 1, 2, 3, 4]

Pero copy.copy() es un método más generico por cuanto actúa sobre cualquier objeto mutable. Por ejemplo, sobre diccionarios.

>>> d1 = {"Hola": "Hello", "Mundo": "World"}
>>> d2 = copy.copy(d1)

Ahora bien, este método únicamente se encarga de copiar el objeto que hemos pasado como argumento, independientemente si contiene en su interior otros objetos mutables. Por ejemplo, cuando tenemos listas dentro de otras listas.

>>> a = [[1, 2], [3, 4]]
>>> b = copy.copy(a)
>>> b[1].append(5)
>>> a
[[1, 2], [3, 4, 5]]

En este caso, efectivamente a es un objeto distinto de b, pero ambos contienen referencias a los mismos objetos dentro de ellos: las listas [1, 2] y [3, 4].

>>> a[1] is b[1]
True

Para encarar este problema hacemos uso de copy.deepcopy(), que se encarga de copiar todos los objetos de forma recursiva.

>>> a = [[1, 2], [3, 4]]
>>> b = copy.deepcopy(a)
>>> b[1].append(5)
>>> a
[[1, 2], [3, 4]]
>>> b
[[1, 2], [3, 4, 5]]
>>> a[1] is b[1]
False

Este método es especialmente útil al momento de crear copias de instacias de clases mutables (recordemos que toda clase es mutable por defecto). Por ejemplo, consideremos la siguiente clase que representa un alumno y tiene como atributo una lista que contiene las materias a las que está inscripto.

class Student:
    def __init__(self, subjects):
        self.subjects = subjects

student1 = Student(["Matemática", "Geografía"])

Pues bien, si tenemos otro estudiante que comparte las mismas materias con student1 pero añade alguna otra, debieramos hacer:

student2 = copy.deepcopy(student1)
student2.subjects.append("Literatura")

E imprimiendo ambos valores vemos que cada objeto mantiene su independencia.

print(student1.subjects)  # ["Matemática", "Geografía"]
print(student2.subjects)  # ["Matemática", "Geografía", "Literatura"]

Tanto copy.copy() como copy.deepcopy() lanzan la excepción copy.error en caso que ocurra un error interno, y TypeError cuando se pasa como argumento un objeto que no puede ser copiado (por ejemplo, un socket).



Deja un comentario