36

I would like to make a copy of a class instance in python. I tried copy.deepcopy but I get the error message:

RuntimeError: Only Variables created explicitly by the user (graph leaves) support the deepcopy protocol at the moment

So suppose I have something like:

class C(object):
    def __init__(self,a,b, **kwargs):
        self.a=a
        self.b=b
        for x, v in kwargs.items():
            setattr(self, x, v)

c = C(4,5,'r'=2)
c.a = 11
del c.b

And now I want to make an identical deep copy of c, is there an easy way?

patapouf_ai
  • 17,605
  • 13
  • 92
  • 132
  • 1
    Yes. Definitely. Override the `__copy__` dunder. Or the `__deepcopy__` one, depending on what you need. – cs95 Jan 19 '18 at 10:24
  • Yes, you can use `copy.deepcopy`. so just `c2 = copy.deepcopy(c)` then `vars(c2) == {'a': 11, 'r': 2}` and `vars(c) == {'a': 11, 'r': 2}` but the traceback your are reporting wouldn't be produced by the class definition you gave... – juanpa.arrivillaga Jan 19 '18 at 10:30
  • @cᴏʟᴅsᴘᴇᴇᴅ note, that isn't needed in this case. The `copy` module will handle types that don't define `__copy__` or `__deepcopy__` I don't think this has to do with that linked duplicate, the error message seems to suggest that deep-copy was purposefully overriden to throw the runtime error. – juanpa.arrivillaga Jan 19 '18 at 10:38
  • @juanpa.arrivillaga Huh... well... CV as "off-topic" then? I've already voted. – cs95 Jan 19 '18 at 10:39
  • @juanpa.arrivillaga yes, in actual fact I am trying to make a copy of a meta-class of `torch.nn.Module`, but that is a bit complicated, so I am giving a simpler example here. Just stating that in my case `deepcopy` doesnt work, so i want another solution. – patapouf_ai Jan 19 '18 at 12:02

3 Answers3

40

Yes you can make a copy of class instance using deepcopy:

from copy import deepcopy

c = C(4,5,'r'=2)
d = deepcopy(c)

This creates the copy of class instance 'c' in 'd' .

Usman
  • 1,983
  • 15
  • 28
  • 7
    I specifically mention that this doesn't work for me. Because in my case, `deepcopy` gives an error. I specifically don't want a solution using `deepcopy`. – patapouf_ai Jan 19 '18 at 11:57
  • 10
    I was directed here from google for searching "how to deepcopy an instance of a class" and this works for me. So, I have to upvote... – lode Jan 31 '19 at 00:33
  • 6
    `deepcopy` uses `pickle`, which breaks on some objects: `TypeError: can't pickle pygame.Surface objects` -- which is specifically what I was searching for a workaround here. So this doesn't work for me. – Marc Maxmeister Sep 29 '19 at 01:39
  • 4
    @lode Only answers that answer the question are to be upvoted. This is a clear downvote since the answerer either did not read or did not understand the question. Upvoting this only because it was in the search results and helped you to deepcopy a class is no option, it should be downvoted instead. – questionto42 Oct 04 '21 at 09:52
15

One way to do that is by implementing __copy__ in the C Class like so:

class A:
    def __init__(self):
        self.var1 = 1
        self.var2 = 2
        self.var3 = 3

class C(A):
    def __init__(self, a=None, b=None, **kwargs):
        super().__init__()
        self.a = a
        self.b = b
        for x, v in kwargs.items():
            setattr(self, x, v)

    def __copy__(self):
        self.normalizeArgs()
        return C(self.a, self.b, kwargs=self.kwargs)

    # THIS IS AN ADDITIONAL GATE-KEEPING METHOD TO ENSURE 
    # THAT EVEN WHEN PROPERTIES ARE DELETED, CLONED OBJECTS
    # STILL GETS DEFAULT VALUES (NONE, IN THIS CASE)
    def normalizeArgs(self):
        if not hasattr(self, "a"):
            self.a      = None
        if not hasattr(self, "b"):
            self.b      = None
        if not hasattr(self, "kwargs"):
            self.kwargs = {}

cMain   = C(a=4, b=5, kwargs={'r':2})

del cMain.b
cClone  = cMain.__copy__()

cMain.a = 11

del  cClone.b
cClone2 = cClone.__copy__()

print(vars(cMain))
print(vars(cClone))
print(vars(cClone2))

enter image description here

Poiz
  • 7,611
  • 2
  • 15
  • 17
  • This doesn't work, I get the error ` 9 def __copy__(self): ---> 10 keywordArgs = vars(self)['kwargs'] 11 return C(self.a, self.b, kwargs=keywordArgs) 12 KeyError: 'kwargs'` – patapouf_ai Jan 19 '18 at 13:28
  • Not also that it should work even after I delete a variable. So after doing `del c.a` I should be able to do `c_prime = c.__copy__()`. – patapouf_ai Jan 19 '18 at 13:29
  • And it should also deep copy the arguments. So if `c.a == [1,2]` I should not have `c.a == c_prime.a`. – patapouf_ai Jan 19 '18 at 13:30
  • it runs. But it doesn't solve my problem because, among other problems mentionned above. If I add the following line at the end of script: `cClone = cMain.__copy__()`, (i.e. after the `del` line), then I get the following error: `AttributeError: 'C' object has no attribute 'b'` – patapouf_ai Jan 19 '18 at 13:47
  • It also doesn't work if I exchange your line `cMain = C(a=4, b=5, kwargs={'r':2})` for the line `cMain = C(4,5,'r'=2)` as I have in my question. – patapouf_ai Jan 19 '18 at 13:50
  • The last problem can be solved by having: ` def __copy__(self): \n keywordArgs = vars(self) \n return C(**keywordArgs)` instead. – patapouf_ai Jan 19 '18 at 13:53
  • Yet another problem with your solution is that if I have `cMain = C([], 1, r=3)` and then `cClone = cMain.__copy__()`, then `cMain.a.append(1)`, then I have `cClone.a == [1]` but it should be `[]`. – patapouf_ai Jan 19 '18 at 13:56
  • @patapouf_ai You can implement a `gatekeeper` Method to account for those Scenarios... The Snippet above was updated to illustrate that... Please, comment if it's still not working for your use-case... Also refer to: https://eval.in/938243 – Poiz Jan 19 '18 at 15:11
8

I have mostly figured it out. The only problem which I cannot overcome is knowing an acceptable set of initialization arguments (arguments for __init__) for all classes. So I have to make the following two assumtions:

1) I have a set of default arguments for class C which I call argsC. 2) All objects in C can be initialized with empty arguments.

In which case I can First: Initialize a new instance of the class C from it's instance which I want to copy c:

c_copy = c.__class__(**argsC)

Second: Go through all the attributes of c and set the attributes c_copy to be a copy of the attributes of c

for att in c.__dict__:
    setattr(c_copy, att, object_copy(getattr(c,att)))

where object_copy is a recursive application of the function we are building.

Last: Delete all attributes in c_copy but not in c:

for att in c_copy.__dict__:
    if not hasattr(c, att):
        delattr(c_copy, att)

Putting this all together we have:

import copy

def object_copy(instance, init_args=None):
    if init_args:
        new_obj = instance.__class__(**init_args)
    else:
        new_obj = instance.__class__()
    if hasattr(instance, '__dict__'):
        for k in instance.__dict__ :
            try:
                attr_copy = copy.deepcopy(getattr(instance, k))
            except Exception as e:
                attr_copy = object_copy(getattr(instance, k))
            setattr(new_obj, k, attr_copy)

        new_attrs = list(new_obj.__dict__.keys())
        for k in new_attrs:
            if not hasattr(instance, k):
                delattr(new_obj, k)
        return new_obj
    else:
        return instance

So putting it all together we have:

argsC = {'a':1, 'b':1}
c = C(4,5,r=[[1],2,3])
c.a = 11
del c.b
c_copy = object_copy(c, argsC)
c.__dict__

{'a': 11, 'r': [[1], 2, 3]}

c_copy.__dict__

{'a': 11, 'r': [[1], 2, 3]}

c.__dict__

{'a': 11, 'r': [[1, 33], 2, 3]}

c_copy.__dict__

{'a': 11, 'r': [[1], 2, 3]}

Which is the desired outcome. It uses deepcopy if it can, but for the cases where it would raise an exception, it can do without.

patapouf_ai
  • 17,605
  • 13
  • 92
  • 132