¿Cómo anular las operaciones de copia / copia profunda para un objeto Python?


101

Entiendo la diferencia entre copyvs. deepcopyen el módulo de copia. He usado copy.copyy copy.deepcopyantes con éxito, pero esta es la primera vez que sobrecargo los métodos __copy__y __deepcopy__. Ya he buscado en Google y miré a través de la incorporada en los módulos de Python para buscar instancias de la __copy__y __deepcopy__funciones (por ejemplo sets.py, decimal.pyy fractions.py), pero todavía no estoy 100% seguro de que tengo derecho.

Aquí está mi escenario:

Tengo un objeto de configuración. Inicialmente, voy a crear una instancia de un objeto de configuración con un conjunto de valores predeterminado. Esta configuración se transferirá a varios otros objetos (para garantizar que todos los objetos comiencen con la misma configuración). Sin embargo, una vez que comienza la interacción del usuario, cada objeto debe modificar sus configuraciones de forma independiente sin afectar las configuraciones de los demás (lo que me dice que tendré que hacer copias en profundidad de mi configuración inicial para entregar).

Aquí hay un objeto de muestra:

class ChartConfig(object):

    def __init__(self):

        #Drawing properties (Booleans/strings)
        self.antialiased = None
        self.plot_style = None
        self.plot_title = None
        self.autoscale = None

        #X axis properties (strings/ints)
        self.xaxis_title = None
        self.xaxis_tick_rotation = None
        self.xaxis_tick_align = None

        #Y axis properties (strings/ints)
        self.yaxis_title = None
        self.yaxis_tick_rotation = None
        self.yaxis_tick_align = None

        #A list of non-primitive objects
        self.trace_configs = []

    def __copy__(self):
        pass

    def __deepcopy__(self, memo):
        pass 

¿Cuál es la forma correcta de implementar los métodos copyy deepcopyen este objeto para garantizar copy.copyy copy.deepcopydarme el comportamiento adecuado?


¿Funciona? ¿Hay problemas?
Ned Batchelder

Pensé que todavía tenía problemas con las referencias compartidas, pero es muy posible que me equivoque en otro lugar. Verificaré dos veces según la publicación de @ MortenSiebuhr cuando tenga la oportunidad y actualizaré con los resultados.
Brent escribe código

De mi comprensión actualmente limitada, esperaría que copy.deepcopy (ChartConfigInstance) devuelva una nueva instancia que no tendría ninguna referencia compartida con el original (sin volver a implementar la copia profunda usted mismo). ¿Es esto incorrecto?
emschorsch

Respuestas:


82

Las recomendaciones para la personalización se encuentran al final de la página de documentos :

Las clases pueden usar las mismas interfaces para controlar la copia que usan para controlar el decapado. Consulte la descripción del módulo pickle para obtener información sobre estos métodos. El módulo de copia no utiliza el módulo de registro copy_reg.

Para que una clase defina su propia implementación de copia, puede definir métodos especiales __copy__()y __deepcopy__(). El primero se llama para implementar la operación de copia superficial; no se pasan argumentos adicionales. Este último está llamado a implementar la operación de copia profunda; se pasa un argumento, el diccionario memo. Si la __deepcopy__() implementación necesita hacer una copia profunda de un componente, debe llamar a la deepcopy()función con el componente como primer argumento y el diccionario memo como segundo argumento.

Dado que parece que no le importa la personalización del decapado, definir __copy__y __deepcopy__definitivamente parece ser el camino correcto para usted.

Específicamente, __copy__(la copia superficial) es bastante fácil en su caso ...:

def __copy__(self):
  newone = type(self)()
  newone.__dict__.update(self.__dict__)
  return newone

__deepcopy__sería similar (aceptando un memoarg también) pero antes de la devolución tendría que solicitar self.foo = deepcopy(self.foo, memo)cualquier atributo self.fooque necesite copia profunda (esencialmente atributos que son contenedores: listas, dictados, objetos no primitivos que contienen otras cosas a través de sus __dict__s).


1
@kaizer, están bien para personalizar el decapado / despegado, así como la copia, pero si no le importa el decapado, es más sencillo y directo de usar __copy__/ __deepcopy__.
Alex Martelli

4
Eso no parece ser una traducción directa de copy / deepcopy. Ni copy ni deepcopy llaman al constructor del objeto que se está copiando. Considere este ejemplo. class Test1 (objeto): def init __ (self): print "% s.% s"% (self .__ class .__ name__, " init ") class Test2 (Test1): def __copy __ (self): new = type (self) () return new t1 = Test1 () copy.copy (t1) t2 = Test2 () copy.copy (t2)
Rob Young

12
Creo que en lugar de type (self) (), deberías usar cls = self .__ class__; cls .__ new __ (cls) para ser insensible a la interfaz de constructores (especialmente para subclases). Sin embargo, aquí no es realmente importante.
Juh_

11
¿Por qué self.foo = deepcopy(self.foo, memo)...? ¿No te refieres realmente newone.foo = ...?
Alois Mahdal

4
El comentario de @ Juh_ es acertado. No quieres llamar __init__. Eso no es lo que hace la copia. También hay muy a menudo un caso de uso en el que el decapado y la copia deben ser diferentes. De hecho, ni siquiera sé por qué copy intenta utilizar el protocolo de decapado de forma predeterminada. La copia es para la manipulación en memoria, el decapado es para la persistencia entre épocas; son cosas completamente diferentes que tienen poca relación entre sí.
Nimrod

96

Al juntar la respuesta de Alex Martelli y el comentario de Rob Young, se obtiene el siguiente código:

from copy import copy, deepcopy

class A(object):
    def __init__(self):
        print 'init'
        self.v = 10
        self.z = [2,3,4]

    def __copy__(self):
        cls = self.__class__
        result = cls.__new__(cls)
        result.__dict__.update(self.__dict__)
        return result

    def __deepcopy__(self, memo):
        cls = self.__class__
        result = cls.__new__(cls)
        memo[id(self)] = result
        for k, v in self.__dict__.items():
            setattr(result, k, deepcopy(v, memo))
        return result

a = A()
a.v = 11
b1, b2 = copy(a), deepcopy(a)
a.v = 12
a.z.append(5)
print b1.v, b1.z
print b2.v, b2.z

huellas dactilares

init
11 [2, 3, 4, 5]
11 [2, 3, 4]

aquí se completa __deepcopy__el memodict para evitar el exceso de copias en caso de que el objeto en sí sea referenciado desde su miembro.


2
@bytestorm ¿qué es Transporter?
Antony Hatchkins

@AntonyHatchkins Transporteres el nombre de mi clase que estoy escribiendo. Para esa clase, quiero anular el comportamiento de la copia profunda.
bytestorm

1
@bytestorm ¿cuál es el contenido Transporter?
Antony Hatchkins

1
Creo que __deepcopy__debería incluir una prueba para evitar la recursividad infinita: <! - language: lang-python -> d = id (self) result = memo.get (d, None) si el resultado no es None: return result
Antonín Hoskovec

@AntonyHatchkins No está claro de inmediato en su publicación dónde memo[id(self)] se acostumbra realmente para evitar la recursividad infinita. He reunido un breve ejemplo que sugiere que copy.deepcopy()internamente aborta la llamada a un objeto si id()es una clave de memo, ¿correcto? También vale la pena señalar que deepcopy()parece hacer esto por sí solo de forma predeterminada , lo que hace que sea difícil imaginar un caso en el que la definición __deepcopy__manual sea realmente necesaria ...
Jonathan H

14

Siguiendo la excelente respuesta de Peter , para implementar una copia profunda personalizada, con una alteración mínima a la implementación predeterminada (por ejemplo, simplemente modificando un campo como yo necesitaba):

class Foo(object):
    def __deepcopy__(self, memo):
        deepcopy_method = self.__deepcopy__
        self.__deepcopy__ = None
        cp = deepcopy(self, memo)
        self.__deepcopy__ = deepcopy_method
        cp.__deepcopy__ = deepcopy_method

        # custom treatments
        # for instance: cp.id = None

        return cp

1
¿Es preferible usarlo delattr(self, '__deepcopy__')entonces setattr(self, '__deepcopy__', deepcopy_method)?
joel

Según esta respuesta , ambos son equivalentes; pero setattr es más útil cuando se establece un atributo cuyo nombre es dinámico / desconocido en el momento de la codificación.
Eino Gourdin

8

Su problema no deja claro por qué necesita anular estos métodos, ya que no desea personalizar los métodos de copia.

De todos modos, si desea personalizar la copia profunda (por ejemplo, compartiendo algunos atributos y copiando otros), aquí tiene una solución:

from copy import deepcopy


def deepcopy_with_sharing(obj, shared_attribute_names, memo=None):
    '''
    Deepcopy an object, except for a given list of attributes, which should
    be shared between the original object and its copy.

    obj is some object
    shared_attribute_names: A list of strings identifying the attributes that
        should be shared between the original and its copy.
    memo is the dictionary passed into __deepcopy__.  Ignore this argument if
        not calling from within __deepcopy__.
    '''
    assert isinstance(shared_attribute_names, (list, tuple))
    shared_attributes = {k: getattr(obj, k) for k in shared_attribute_names}

    if hasattr(obj, '__deepcopy__'):
        # Do hack to prevent infinite recursion in call to deepcopy
        deepcopy_method = obj.__deepcopy__
        obj.__deepcopy__ = None

    for attr in shared_attribute_names:
        del obj.__dict__[attr]

    clone = deepcopy(obj)

    for attr, val in shared_attributes.iteritems():
        setattr(obj, attr, val)
        setattr(clone, attr, val)

    if hasattr(obj, '__deepcopy__'):
        # Undo hack
        obj.__deepcopy__ = deepcopy_method
        del clone.__deepcopy__

    return clone



class A(object):

    def __init__(self):
        self.copy_me = []
        self.share_me = []

    def __deepcopy__(self, memo):
        return deepcopy_with_sharing(self, shared_attribute_names = ['share_me'], memo=memo)

a = A()
b = deepcopy(a)
assert a.copy_me is not b.copy_me
assert a.share_me is b.share_me

c = deepcopy(b)
assert c.copy_me is not b.copy_me
assert c.share_me is b.share_me

¿No necesita el clon también el __deepcopy__reinicio de su método, ya que tendrá __deepcopy__= Ninguno?
flutefreak7

2
No Si __deepcopy__no se encuentra el método (o obj.__deepcopy__devuelve Ninguno), deepcopyrecurre a la función estándar de copia profunda. Esto se puede ver aquí
Peter

1
¿Pero entonces b no tendrá la capacidad de realizar copias en profundidad al compartir? c = deepcopy (a) sería diferente de d = deepcopy (b) porque d sería un deepcopy predeterminado donde c tendría algunos atributos compartidos con a.
flutefreak7

1
Ah, ahora veo lo que estás diciendo. Buen punto. Lo arreglé, creo, eliminando el __deepcopy__=Noneatributo falso del clon. Ver nuevo código.
Peter

1
tal vez sea claro para los expertos en Python: si usa este código en Python 3, cambie "por atributo, val en shared_attributes.iteritems ():" con "para atributo, val en shared_attributes.items ():"
complexM

6

Puede que esté un poco fuera de lugar en los detalles, pero aquí va;

De los copydocumentos ;

  • Una copia superficial construye un nuevo objeto compuesto y luego (en la medida de lo posible) inserta en él referencias a los objetos encontrados en el original.
  • Una copia profunda construye un nuevo objeto compuesto y luego, de forma recursiva, inserta copias en él de los objetos encontrados en el original.

En otras palabras: copy()copiará solo el elemento superior y dejará el resto como punteros en la estructura original. deepcopy()copiará de forma recursiva sobre todo.

Es decir, deepcopy()es lo que necesitas.

Si necesita hacer algo realmente específico, puede anular __copy__()o __deepcopy__(), como se describe en el manual. Personalmente, probablemente implementaría una función simple (por ejemplo, config.copy_config()o tal) para dejar claro que no es el comportamiento estándar de Python.


3
Para que una clase defina su propia implementación de copia, puede definir métodos especiales __copy__() y __deepcopy__(). docs.python.org/library/copy.html
SilentGhost

Volveré a comprobar mi código, gracias. Me sentiré tonto si esto fuera un error simple en otro lugar :-P
Brent escribe código

@MortenSiebuhr Tienes razón. No estaba del todo claro que copy / deepcopy haría cualquier cosa de forma predeterminada sin que yo anulara esas funciones. Sin embargo, estaba buscando un código real que pueda modificar más tarde (por ejemplo, si no quiero copiar todos los atributos), así que les di un voto a favor, pero voy a ir con la respuesta de @ AlexMartinelli. ¡Gracias!
Brent escribe código

2

El copymódulo usa eventualmente el protocolo__getstate__() / pickling , por lo que estos también son objetivos válidos para anular.__setstate__()

La implementación predeterminada simplemente regresa y establece el __dict__de la clase, por lo que no tiene que llamar super()ni preocuparse por el ingenioso truco de Eino Gourdin, arriba .


1

Sobre la base de la respuesta limpia de Antony Hatchkins, aquí está mi versión en la que la clase en cuestión deriva de otra clase personalizada (a la que debemos llamar super):

class Foo(FooBase):
    def __init__(self, param1, param2):
        self._base_params = [param1, param2]
        super(Foo, result).__init__(*self._base_params)

    def __copy__(self):
        cls = self.__class__
        result = cls.__new__(cls)
        result.__dict__.update(self.__dict__)
        super(Foo, result).__init__(*self._base_params)
        return result

    def __deepcopy__(self, memo):
        cls = self.__class__
        result = cls.__new__(cls)
        memo[id(self)] = result
        for k, v in self.__dict__.items():
            setattr(result, k, copy.deepcopy(v, memo))
        super(Foo, result).__init__(*self._base_params)
        return result
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.