Come sovrascrivere le operazioni di copia / copia profonda per un oggetto Python?


101

Capisco la differenza tra copyvs. deepcopynel modulo di copia. Ho usato copy.copye copy.deepcopyprima con successo, ma questa è la prima volta che ho effettivamente sovraccaricato i metodi __copy__e __deepcopy__. Ho già Googled intorno e guardato attraverso il built-in moduli Python per cercare istanze del __copy__e __deepcopy__funzioni (ad esempio sets.py, decimal.pye fractions.py), ma non sono ancora sicuro al 100% che ho capito bene.

Ecco il mio scenario:

Ho un oggetto di configurazione. Inizialmente, creerò un'istanza di un oggetto di configurazione con un set di valori predefinito. Questa configurazione verrà trasferita a più altri oggetti (per garantire che tutti gli oggetti inizino con la stessa configurazione). Tuttavia, una volta avviata l'interazione con l'utente, ogni oggetto deve modificare le proprie configurazioni in modo indipendente senza influire sulle configurazioni dell'altro (il che mi dice che dovrò fare delle copie profonde della mia configurazione iniziale da consegnare).

Ecco un oggetto di esempio:

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 

Qual è il modo giusto per implementare i metodi copye deepcopysu questo oggetto per garantire copy.copye copy.deepcopyfornire il comportamento corretto?


Funziona? Ci sono problemi?
Ned Batchelder,

Pensavo di avere ancora problemi con i riferimenti condivisi, ma è del tutto possibile che abbia sbagliato altrove. Controllerò due volte in base al post di @ MortenSiebuhr quando avrò la possibilità e aggiornerò con i risultati.
Codice di scrittura Brent,

Dalla mia comprensione attualmente limitata, mi aspetterei che copy.deepcopy (ChartConfigInstance) restituisse una nuova istanza che non avrebbe alcun riferimento condiviso con l'originale (senza reimplementare tu stesso la copia profonda). È sbagliato?
emschorsch

Risposte:


82

I consigli per la personalizzazione si trovano alla fine della pagina dei documenti :

Le classi possono utilizzare le stesse interfacce per controllare la copia che usano per controllare il decapaggio. Vedere la descrizione del modulo pickle per informazioni su questi metodi. Il modulo di copia non utilizza il modulo di registrazione copy_reg.

Affinché una classe possa definire la propria implementazione di copia, può definire metodi speciali __copy__()e __deepcopy__(). Il primo è chiamato per implementare l'operazione di copia superficiale; non vengono passati argomenti aggiuntivi. Quest'ultimo è chiamato per implementare l'operazione di copia profonda; viene passato un argomento, il dizionario memo. Se l' __deepcopy__() implementazione deve creare una copia completa di un componente, dovrebbe chiamare la deepcopy()funzione con il componente come primo argomento e il dizionario dei memo come secondo argomento.

Dal momento che sembra che non ti interessi della personalizzazione del decapaggio, definire __copy__e __deepcopy__sicuramente sembra la strada giusta per te.

In particolare, __copy__(la copia superficiale) è abbastanza facile nel tuo caso ...:

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

__deepcopy__sarebbe simile (accettando anche un memoarg) ma prima del ritorno dovrebbe chiamare self.foo = deepcopy(self.foo, memo)qualsiasi attributo self.fooche necessita di una copia profonda (essenzialmente attributi che sono contenitori - elenchi, dettami, oggetti non primitivi che contengono altre cose attraverso i loro __dict__).


1
@kaizer, vanno bene per personalizzare il decapaggio / unpickling così come la copia, ma se non ti interessa il decapaggio, è più semplice e diretto da usare __copy__/ __deepcopy__.
Alex Martelli

4
Questa non sembra essere una traduzione diretta di copy / deepcopy. Né copy né deepcopy chiamano il costruttore dell'oggetto da copiare. Considera questo esempio. class Test1 (oggetto): 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
Penso che invece di type (self) (), dovresti usare cls = self .__ class__; cls .__ new __ (cls) per essere insensibile all'interfaccia dei costruttori (specialmente per le sottoclassi). Tuttavia, qui non è molto importante.
Juh_

11
Perché self.foo = deepcopy(self.foo, memo)...? Non intendi davvero newone.foo = ...?
Alois Mahdal

4
Il commento di @ Juh_ è perfetto. Non vuoi chiamare __init__. Non è quello che fa la copia. Inoltre c'è molto spesso un caso d'uso in cui il decapaggio e la copia devono essere diversi. In effetti, non so nemmeno perché la copia cerchi di utilizzare il protocollo di decapaggio per impostazione predefinita. La copia è per la manipolazione in memoria, il decapaggio è per la persistenza tra le epoche; sono cose completamente diverse che hanno poca relazione l'una con l'altra.
Nimrod

97

Mettendo insieme la risposta di Alex Martelli e il commento di Rob Young si ottiene il seguente codice:

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

stampe

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

qui __deepcopy__riempie il memodict per evitare una copia eccessiva nel caso in cui l'oggetto stesso sia referenziato dal suo membro.


2
@bytestorm cos'è Transporter?
Antony Hatchkins

@AntonyHatchkins Transporterè il nome della mia classe che sto scrivendo. Per quella classe voglio sovrascrivere il comportamento di copia profonda.
Tempesta

1
@bytestorm qual è il contenuto di Transporter?
Antony Hatchkins

1
Penso che __deepcopy__dovrebbe includere un test per evitare la ricorsione infinita: <! - language: lang-python -> d = id (self) result = memo.get (d, None) se il risultato non è None: return result
Antonín Hoskovec

@AntonyHatchkins Non è immediatamente chiaro dal tuo post dove memo[id(self)] viene effettivamente utilizzato per prevenire la ricorsione infinita. Ho messo insieme un breve esempio che suggerisce che copy.deepcopy()interrompe internamente la chiamata a un oggetto se id()è una chiave di memo, corretto? Vale anche la pena notare che deepcopy()sembra farlo da solo per impostazione predefinita , il che rende difficile immaginare un caso in cui __deepcopy__è effettivamente necessaria la definizione manuale ...
Jonathan H

14

Seguendo l'eccellente risposta di Peter , per implementare una deepcopy personalizzata, con una modifica minima all'implementazione predefinita (ad esempio, solo modificando un campo come avevo bisogno):

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
è preferibile utilizzare delattr(self, '__deepcopy__')questo setattr(self, '__deepcopy__', deepcopy_method)?
joel

Secondo questa risposta , entrambi sono equivalenti; ma setattr è più utile quando si imposta un attributo il cui nome è dinamico / non noto al momento della codifica.
Eino Gourdin

8

Non è chiaro dal tuo problema il motivo per cui devi sovrascrivere questi metodi, dal momento che non vuoi personalizzare i metodi di copia.

Ad ogni modo, se vuoi personalizzare il deep copy (es. Condividendo alcuni attributi e copiandone altri), ecco una soluzione:

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

Il clone non necessita anche del __deepcopy__ripristino del metodo poiché avrà __deepcopy__= Nessuno?
flutefreak7

2
No. Se il __deepcopy__metodo non viene trovato (o obj.__deepcopy__restituisce Nessuno), deepcopyricorre alla funzione di copia profonda standard. Questo può essere visto qui
Peter

1
Ma allora b non avrà la capacità di copiare in profondità con la condivisione? c = deepcopy (a) sarebbe diverso da d = deepcopy (b) perché d sarebbe una deepcopy predefinita dove c avrebbe alcuni attributi condivisi con a.
flutefreak7

1
Ah, ora capisco cosa stai dicendo. Buon punto. L'ho risolto, credo, eliminando l' __deepcopy__=Noneattributo fake dal clone. Vedi nuovo codice.
Peter

1
forse chiaro agli esperti di python: se usi questo codice in python 3, cambia "per attr, val in shared_attributes.iteritems ():" con "per attr, val in shared_attributes.items ():"
complexM

6

Potrei essere un po 'fuori dai dettagli, ma qui va;

Dai copydocumenti ;

  • Una copia superficiale costruisce un nuovo oggetto composto e quindi (per quanto possibile) vi inserisce riferimenti agli oggetti trovati nell'originale.
  • Una copia profonda costruisce un nuovo oggetto composto e quindi, in modo ricorsivo, inserisce in esso copie degli oggetti trovati nell'originale.

In altre parole: copy()copierà solo l'elemento superiore e lascerà il resto come puntatori nella struttura originale. deepcopy()copierà ricorsivamente tutto.

Cioè, deepcopy()è quello che ti serve.

Se devi fare qualcosa di veramente specifico, puoi sovrascrivere __copy__()o __deepcopy__(), come descritto nel manuale. Personalmente, probabilmente implementerei una semplice funzione (ad esempio config.copy_config()o simile) per rendere chiaro che non si tratta di un comportamento standard di Python.


3
Affinché una classe possa definire la propria implementazione di copia, può definire metodi speciali __copy__() e __deepcopy__(). docs.python.org/library/copy.html
SilentGhost,

Ricontrollo il mio codice, grazie. Mi sentirò stupido se questo fosse un semplice bug altrove :-P
Brent Writes Code

@MortenSiebuhr Hai ragione. Non ero del tutto chiaro che copy / deepcopy avrebbe fatto qualsiasi cosa per impostazione predefinita senza che io sovrascrivessi quelle funzioni. Stavo cercando il codice effettivo che posso modificare in seguito (ad esempio se non voglio copiare tutti gli attributi), quindi ti ho dato un voto positivo ma vado con la risposta di @ AlexMartinelli. Grazie!
Codice di scrittura Brent

2

Il copymodulo utilizza eventualmente il protocollo__getstate__() / pickling , quindi anche questi sono obiettivi validi da sovrascrivere.__setstate__()

L'implementazione predefinita restituisce e imposta solo il valore __dict__della classe, quindi non devi chiamare super()e preoccuparti del trucco intelligente di Eino Gourdin, sopra .


1

Basandosi sulla risposta pulita di Antony Hatchkins, ecco la mia versione in cui la classe in questione deriva da un'altra classe personalizzata (st dobbiamo chiamare 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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.