3
\$\begingroup\$

For a given object that may be an instance of a class, a built-in container or type, I would like to copy that object, but replace all instances of a given class with something else. I can do this with two calls to deepcopy as shown below, but there may be a more straightforward approach.

Create some complex nested object:

class T(object):
    pass

class A(object):
    def __init__(self):
        self.x = 10
        self.t1 = T()

class B(object):
    def __init__(self):
        self.a = A()
        self.y = 20
        self.t2 = T()

b = B()

Get a mapping from ids to objects:

visited = {}
copy.deepcopy(b, visited)

Create a custom dict type to replace instances of T with 100:

import collections

class Replacer(collections.MutableMapping):
    def __init__(self, idmap):
        self.store = dict()
        self.idmap = idmap
    def __getitem__(self, key):
        if isinstance(self.idmap[key], T):
            return 100
        return self.store[key]
    def __setitem__(self, key, value):
        self.store[key] = value
    def __delitem__(self, key):
        del self.store[key]
    def __iter__(self):
        return iter(self.store)
    def __len__(self):
        return len(self.store)

Copy and replace all T instances:

bb = copy.deepcopy(b, Replacer(visited))

In [121]: bb.t2
Out[121]: 100

In [122]: bb.a.t1
Out[122]: 100
\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

You are overly complicating what you are trying to achieve by defining your Replacer class. You need two passes because your logic is:

  1. store each id of custom objects into visited, associated to a copy of it;
  2. change the value associated to ids of T objects into 100.

See how in 1. you already made a copy? You don't need that, you need to change how objects are copied in the first place.

The deepcopy function tries to copy a custom object x using one of 3 functions, in that order:

  • x.__deepcopy__;
  • x.__reduce_ex__(2);
  • x.__reduce__().

__reduce_ex__ and __reduce__ are a bit special and provided by object by default so better not to touch it. __deepcopy__, however, is copy.deepcopy own mechanism and should be used anytime you want to change how deepcopy should create new objects out of old ones.

If all you want to do is to turn all Ts into 100 when deepcopying an arbitrary object, then all you have to do is to implement T.__deepcopy__ in such a way:

class T(object):
    def __deepcopy__(self, memo): # expected signature for copy.deepcopy
        return 100

Every deepcopy will then turn each T into 100:

  • Ex1:

    >>> b = [1,2,"hello",T(),('a',3,T()),T()]
    >>> copy.deepcopy(b)
    [1, 2, 'hello', 100, ('a', 3, 100), 100]
    
  • Ex2, using your A and B classes:

    >>> b = B()
    >>> bb = copy.deepcopy(b)
    >>> bb.t2
    100
    >>> bb.a.t1
    100
    

Note that in the context of your implementation and various calls, the T.__deepcopy__'s memo parameter would be an alias to visited, use it as you see fit if you want to implement custom logic. Read the last paragraph of the documentation for specific use cases.

\$\endgroup\$
0
\$\begingroup\$

Just to build on what Mathias was saying, if I wanted to have some additional logic for how to replace instances of T, I could use a custom memo as follows.

class Replacer(dict):
    def __init__(self, *args, **kwargs):
        dict.__init__(self, *args, **kwargs)
    def replace_t(self, t):
        return t.idx * 100

class T(object):
    def __init__(self, idx):
        self.idx = idx
    def __deepcopy__(self, memo):
        return memo.replace_t(self)

Usage:

In [25]: b = B()

In [26]: bb = copy.deepcopy(b, Replacer())

In [27]: bb.t2
Out[27]: 200

In [28]: bb.a.t1
Out[28]: 100
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.