Comment remplacer les opérations de copie / copie profonde pour un objet Python?


101

Je comprends la différence entre copyet deepcopydans le module de copie. J'ai utilisé copy.copyet copy.deepcopyavant avec succès, mais c'est la première fois que je surcharge les méthodes __copy__et __deepcopy__. Je l' ai déjà googlé autour et regardé à travers le haut-modules Python pour rechercher des instances du __copy__et des __deepcopy__fonctions (par exemple sets.py, decimal.pyet fractions.py), mais je ne suis pas encore à 100% sûr que je l' ai raison.

Voici mon scénario:

J'ai un objet de configuration. Au départ, je vais instancier un objet de configuration avec un ensemble de valeurs par défaut. Cette configuration sera transmise à plusieurs autres objets (pour garantir que tous les objets démarrent avec la même configuration). Cependant, une fois que l'interaction utilisateur commence, chaque objet doit modifier ses configurations indépendamment sans affecter les configurations de l'autre (ce qui me dit que je vais devoir faire des copies en profondeur de ma configuration initiale à remettre).

Voici un exemple d'objet:

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 

Quelle est la bonne façon d'implémenter les méthodes copyet deepcopysur cet objet pour m'assurer copy.copyet copy.deepcopyme donner le bon comportement?


Est-ce que ça marche? Y a-t-il des problèmes?
Ned Batchelder

Je pensais avoir encore des problèmes avec les références partagées, mais il est tout à fait possible que je me sois trompé ailleurs. Je vérifierai en fonction de la publication de @ MortenSiebuhr lorsque j'en aurai l'occasion et je mettrai à jour les résultats.
Brent écrit le code le

D'après ma compréhension actuellement limitée, je m'attendrais à ce que copy.deepcopy (ChartConfigInstance) renvoie une nouvelle instance qui n'aurait aucune référence partagée avec l'original (sans réimplémenter deepcopy vous-même). Est-ce incorrect?
emschorsch

Réponses:


82

Les recommandations de personnalisation se trouvent à la toute fin de la page de documentation :

Les classes peuvent utiliser les mêmes interfaces pour contrôler la copie qu'elles utilisent pour contrôler le décapage. Voir la description du module pickle pour plus d'informations sur ces méthodes. Le module de copie n'utilise pas le module d'enregistrement copy_reg.

Pour qu'une classe définisse sa propre implémentation de copie, elle peut définir des méthodes spéciales __copy__()et __deepcopy__(). Le premier est appelé à implémenter l'opération de copie superficielle; aucun argument supplémentaire n'est passé. Ce dernier est appelé à implémenter l'opération de copie profonde; on lui passe un argument, le dictionnaire mémo. Si l' __deepcopy__() implémentation a besoin de faire une copie complète d'un composant, elle doit appeler la deepcopy()fonction avec le composant comme premier argument et le dictionnaire mémo comme second argument.

Puisque vous semblez ne pas vous soucier de la personnalisation du décapage, définir __copy__et __deepcopy__semble définitivement être la bonne voie pour vous.

Plus précisément, __copy__(la copie superficielle) est assez facile dans votre cas ...:

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

__deepcopy__serait similaire (accepter un memoargument aussi) mais avant le retour, il devrait appeler self.foo = deepcopy(self.foo, memo)tout attribut self.foonécessitant une copie approfondie (essentiellement des attributs qui sont des conteneurs - des listes, des dictionnaires, des objets non primitifs qui contiennent d'autres éléments à travers leurs __dict__s).


1
@kaizer, ils permettent de personnaliser le décapage / le décapage ainsi que la copie, mais si vous ne vous souciez pas du décapage, il est plus simple et plus direct d'utiliser __copy__/ __deepcopy__.
Alex Martelli

4
Cela ne semble pas être une traduction directe de copie / copie profonde. Ni copy ni deepcopy n'appellent le constructeur de l'objet copié. Prenons cet exemple. class Test1 (objet): 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
Je pense qu'au lieu de type (self) (), vous devriez utiliser cls = self .__ class__; cls .__ new __ (cls) pour être insensible à l'interface des constructeurs (en particulier pour le sous-classement). Ce n'est pas vraiment important ici cependant.
Juh_

11
Pourquoi self.foo = deepcopy(self.foo, memo)...? Tu ne veux pas vraiment dire newone.foo = ...?
Alois Mahdal

4
Le commentaire de @ Juh_ est parfait. Vous ne voulez pas appeler __init__. Ce n'est pas ce que fait la copie. Il existe également très souvent un cas d'utilisation où le décapage et la copie doivent être différents. En fait, je ne sais même pas pourquoi Copy essaie d'utiliser le protocole de décapage par défaut. La copie est pour la manipulation en mémoire, le décapage est pour la persistance entre les époques; ce sont des choses complètement différentes qui n'ont guère de rapport entre elles.
Nimrod

96

En réunissant la réponse d'Alex Martelli et le commentaire de Rob Young, vous obtenez le code suivant:

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

impressions

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

ici __deepcopy__remplit le memodict pour éviter une copie excessive au cas où l'objet lui-même serait référencé à partir de son membre.


2
@bytestorm qu'est-ce que c'est Transporter?
Antony Hatchkins

@AntonyHatchkins Transporterest le nom de ma classe que j'écris. Pour cette classe, je souhaite remplacer le comportement de deepcopy.
bytestorm

1
@bytestorm quel est le contenu de Transporter?
Antony Hatchkins

1
Je pense que __deepcopy__devrait inclure un test pour éviter la récursivité infinie: <! - language: lang-python -> d = id (self) result = memo.get (d, None) si result is not None: return result
Antonín Hoskovec

@AntonyHatchkins Il n'est pas immédiatement clair d'après votre message memo[id(self)] s'habitue réellement à empêcher une récursivité infinie. J'ai rassemblé un court exemple qui suggère copy.deepcopy()qu'interrompt en interne l'appel à un objet si id()c'est une clé de memo, correct? Il est également intéressant de noter que cela deepcopy()semble faire cela seul par défaut , ce qui rend difficile d'imaginer un cas où la définition __deepcopy__manuelle est réellement nécessaire ...
Jonathan H

14

Suite à l'excellente réponse de Peter , pour implémenter une copie profonde personnalisée, avec une modification minimale de l'implémentation par défaut (par exemple, simplement modifier un champ comme j'avais besoin):

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
est-ce préférable d'utiliser delattr(self, '__deepcopy__')alors setattr(self, '__deepcopy__', deepcopy_method)?
joel le

Selon cette réponse , les deux sont équivalents; mais setattr est plus utile lors de la définition d'un attribut dont le nom est dynamique / inconnu au moment du codage.
Eino Gourdin le

8

La raison pour laquelle vous devez remplacer ces méthodes ne ressort pas clairement de votre problème, car vous ne souhaitez pas personnaliser les méthodes de copie.

Quoi qu'il en soit, si vous souhaitez personnaliser la copie complète (par exemple en partageant certains attributs et en copiant d'autres), voici une solution:

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

Le clone n'a-t-il pas également besoin de __deepcopy__réinitialiser sa méthode car il aura __deepcopy__= None?
flutefreak7

2
Nan. Si la __deepcopy__méthode n'est pas trouvée (ou obj.__deepcopy__renvoie None), alors deepcopyrevient à la fonction standard de copie profonde. Cela peut être vu ici
Peter

1
Mais alors b n'aura pas la capacité de copier en profondeur avec le partage? c = deepcopy (a) serait différent de d = deepcopy (b) car d serait une copie profonde par défaut où c aurait des attrs partagés avec a.
flutefreak7

1
Ah, maintenant je vois ce que vous dites. Bon point. Je l'ai corrigé, je pense, en supprimant le faux __deepcopy__=Noneattribut du clone. Voir le nouveau code.
Peter

1
peut-être clair pour les experts de python: si vous utilisez ce code en python 3, changez "pour attr, val dans shared_attributes.iteritems ():" avec "pour attr, val dans shared_attributes.items ():"
complexM

6

Je pourrais être un peu en retard sur les détails, mais voilà;

À partir des copydocuments ;

  • Une copie superficielle construit un nouvel objet composé puis (dans la mesure du possible) y insère des références aux objets trouvés dans l'original.
  • Une copie complète construit un nouvel objet composé puis, de manière récursive, y insère des copies des objets trouvés dans l'original.

En d'autres termes: copy()copiera uniquement l'élément supérieur et laissera le reste comme pointeurs dans la structure d'origine. deepcopy()copiera récursivement sur tout.

C'est deepcopy()ce dont vous avez besoin.

Si vous avez besoin de faire quelque chose de vraiment spécifique, vous pouvez remplacer __copy__()ou __deepcopy__(), comme décrit dans le manuel. Personnellement, j'implémenterais probablement une fonction simple (par exemple config.copy_config()ou autre) pour indiquer clairement qu'il ne s'agit pas d'un comportement standard Python.


3
Pour qu'une classe définisse sa propre implémentation de copie, elle peut définir des méthodes spéciales __copy__() et __deepcopy__(). docs.python.org/library/copy.html
SilentGhost

Je vais vérifier mon code, merci. Je vais me sentir stupide si c'était un simple bug ailleurs :-P
Brent écrit le code

@MortenSiebuhr Vous avez raison. Je n'étais pas tout à fait clair que la copie / copie profonde ferait n'importe quoi par défaut sans que je ne remplace ces fonctions. Je cherchais un code réel que je pourrais modifier plus tard (par exemple si je ne veux pas copier tous les attributs), donc je vous ai donné un vote positif mais je vais aller avec la réponse de @ AlexMartinelli. Merci!
Brent écrit le code du

2

Le copymodule utilise finalement le protocole__getstate__() / pickling , ce sont donc également des cibles valides à remplacer.__setstate__()

L'implémentation par défaut renvoie et définit simplement le __dict__de la classe, vous n'avez donc pas à appeler super()et à vous soucier de l'astuce intelligente d'Eino Gourdin, ci - dessus .


1

En s'appuyant sur la réponse claire d'Antony Hatchkins, voici ma version où la classe en question dérive d'une autre classe personnalisée (st nous devons appeler 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
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.