copy — Duplicar Objectos

Propósito:Proporciona funciones para duplicar objetos utilizando semántica de copia superficial o profunda.

El módulo copy incluye dos funciones, copy() y deepcopy(), para duplicar objetos existentes.

Copias superficiales

La copia superficial creada por copy() es un contenedor nuevo poblado con referencias a los contenidos del objeto original. Al hacer una copia superficial de un objeto list, una nueva list se construye y los elementos del objeto original se anexan a ésta.

copy_shallow.py
import copy
import functools


@functools.total_ordering
class MyClass:

    def __init__(self, name):
        self.name = name

    def __eq__(self, other):
        return self.name == other.name

    def __gt__(self, other):
        return self.name > other.name


a = MyClass('a')
my_list = [a]
dup = copy.copy(my_list)

print('             my_list:', my_list)
print('                 dup:', dup)
print('      dup is my_list:', (dup is my_list))
print('      dup == my_list:', (dup == my_list))
print('dup[0] is my_list[0]:', (dup[0] is my_list[0]))
print('dup[0] == my_list[0]:', (dup[0] == my_list[0]))

Para una copia superficial, la instancia MyClass no es duplicada, la referencia en la lista dup es a el mismo objeto que está en my_list.

$ python3 copy_shallow.py

             my_list: [<__main__.MyClass object at 0x1007a87b8>]
                 dup: [<__main__.MyClass object at 0x1007a87b8>]
      dup is my_list: False
      dup == my_list: True
dup[0] is my_list[0]: True
dup[0] == my_list[0]: True

Copias profundas

La copia profunda creada por deepcopy() es un contenedor nuevo poblado con copias de los contenidos del objeto original. Para hacer una copia profunda de una list, una nueva lista se construye, los elementos de la lista original se copian, y luego esas copias son anexadas a la nueva lista.

Reemplazar la llamada de copy() con deepcopy() hace visible la diferencia en la salida.

copy_deep.py
import copy
import functools


@functools.total_ordering
class MyClass:

    def __init__(self, name):
        self.name = name

    def __eq__(self, other):
        return self.name == other.name

    def __gt__(self, other):
        return self.name > other.name


a = MyClass('a')
my_list = [a]
dup = copy.deepcopy(my_list)

print('             my_list:', my_list)
print('                 dup:', dup)
print('      dup is my_list:', (dup is my_list))
print('      dup == my_list:', (dup == my_list))
print('dup[0] is my_list[0]:', (dup[0] is my_list[0]))
print('dup[0] == my_list[0]:', (dup[0] == my_list[0]))

El primer elemento de la lista ya no es la misma referencia de objeto, pero cuando se comparan los dos objetos, todavía se evalúan como iguales.

$ python3 copy_deep.py

             my_list: [<__main__.MyClass object at 0x1018a87b8>]
                 dup: [<__main__.MyClass object at 0x1018b1b70>]
      dup is my_list: False
      dup == my_list: True
dup[0] is my_list[0]: False
dup[0] == my_list[0]: True

Personalizando el comportamiento de copia

Es posible controlar cómo se hacen las copias utilizando los métodos especiales __copy__() y __deepcopy__().

  • __copy__() se llama sin ningún argumento y debe devolver un copia superficial del objeto.
  • __deepcopy__() se llama con un diccionario memo y debe devolver una copia profunda del objeto. Cualquier atributo miembro que necesite ser copiado profundamente debe ser pasado a copy.deepcopy(), junto con el diccionario memo, para controlar la recursión. (El diccionario memo se explica con más detalle más adelante).

El siguiente ejemplo ilustra cómo se llaman los métodos.

copy_hooks.py
import copy
import functools


@functools.total_ordering
class MyClass:

    def __init__(self, name):
        self.name = name

    def __eq__(self, other):
        return self.name == other.name

    def __gt__(self, other):
        return self.name > other.name

    def __copy__(self):
        print('__copy__()')
        return MyClass(self.name)

    def __deepcopy__(self, memo):
        print('__deepcopy__({})'.format(memo))
        return MyClass(copy.deepcopy(self.name, memo))


a = MyClass('a')

sc = copy.copy(a)
dc = copy.deepcopy(a)

El diccionario memo se utiliza para realizar un seguimiento de los valores que ya han sido copiados, para evitar recursión infinita.

$ python3 copy_hooks.py

__copy__()
__deepcopy__({})

Recursión en copia profunda

Para evitar problemas con la duplicación de estructuras de datos recursivas, deepcopy() usa un diccionario para rastrear objetos que ya han sido copiados. Este diccionario se pasa al método __deepcopy__() para que pueda examinarse allí también.

El siguiente ejemplo muestra cómo una estructura de datos interconectados como un grafo dirigido puede ayudar a proteger contra la recursión implementando un método __deepcopy__().

copy_recursion.py
import copy


class Graph:

    def __init__(self, name, connections):
        self.name = name
        self.connections = connections

    def add_connection(self, other):
        self.connections.append(other)

    def __repr__(self):
        return 'Graph(name={}, id={})'.format(
            self.name, id(self))

    def __deepcopy__(self, memo):
        print('\nCalling __deepcopy__ for {!r}'.format(self))
        if self in memo:
            existing = memo.get(self)
            print('  Already copied to {!r}'.format(existing))
            return existing
        print('  Memo dictionary:')
        if memo:
            for k, v in memo.items():
                print('    {}: {}'.format(k, v))
        else:
            print('    (empty)')
        dup = Graph(copy.deepcopy(self.name, memo), [])
        print('  Copying to new object {}'.format(dup))
        memo[self] = dup
        for c in self.connections:
            dup.add_connection(copy.deepcopy(c, memo))
        return dup


root = Graph('root', [])
a = Graph('a', [root])
b = Graph('b', [a, root])
root.add_connection(a)
root.add_connection(b)

dup = copy.deepcopy(root)

La clase Graph incluye algunos métodos básicos de gráficos dirigidos. Una instancia se puede inicializar con un nombre y una lista de nodos existentes a los que está conectado. El método add_connection() método se usa para configurar conexiones bidireccionales. También se usa por el operador de copia profunda.

El método __deepcopy__() imprime mensajes para mostrar cómo es llamado, y administra el contenido del diccionario memo según sea necesario. En lugar de copiar toda la lista de conexiones en su totalidad, crea una nueva lista y anexa copias de las conexiones individuales a ésta. Eso asegura que el diccionario memo se actualiza cuando cada nuevo nodo es duplicado, y evita problemas de recursión o copias adicionales de nodos. Como antes, el método devuelve el objeto copiado cuando está listo.

digraph copy_example {
"root";
"a" -> "root";
"b" -> "root";
"b" -> "a";
"root" -> "a";
"root" -> "b";
}

Copia profunda para un grafo de objetos con ciclos.

El grafo que se muestra en the figure incluye varios ciclos, pero manejar la recursión con el diccionario memo evita que el recorrido cause un error de desbordamiento de la pila. Cuando se copia el nodo root, produce el siguiente resultado.

$ python3 copy_recursion.py


Calling __deepcopy__ for Graph(name=root, id=4314569528)
  Memo dictionary:
    (empty)
  Copying to new object Graph(name=root, id=4315093592)

Calling __deepcopy__ for Graph(name=a, id=4314569584)
  Memo dictionary:
    Graph(name=root, id=4314569528): Graph(name=root,
id=4315093592)
  Copying to new object Graph(name=a, id=4315094208)

Calling __deepcopy__ for Graph(name=root, id=4314569528)
  Already copied to Graph(name=root, id=4315093592)

Calling __deepcopy__ for Graph(name=b, id=4315092248)
  Memo dictionary:
    4314569528: Graph(name=root, id=4315093592)
    4315692808: [Graph(name=root, id=4314569528), Graph(name=a,
id=4314569584)]
    Graph(name=root, id=4314569528): Graph(name=root,
id=4315093592)
    4314569584: Graph(name=a, id=4315094208)
    Graph(name=a, id=4314569584): Graph(name=a, id=4315094208)
  Copying to new object Graph(name=b, id=4315177536)

La segunda vez que se encuentra el nodo root, mientras que el nodo a está siendo copiado, __deepcopy__() detecta la recursión y reutiliza el valor existente del diccionario memo en lugar de crear un nuevo objeto.