2

I'm using deepcopy to make a deep copy of a Python class, and change the class variable of the copied object. For some reason, the class variable of the original class that was copied is also being changed. Why is this?

It doesn't seem to happen when I create a class with the 'type' syntax.

import copy

class Test:
    a = 0

print(Test.a) # 0
b = copy.deepcopy(Test)
b.a = 1
print(Test.a) # 1 - why?

c = type('Test2', (Test,), {'a': 2})
print(Test.a) # still 1
fountainhead
  • 3,584
  • 1
  • 8
  • 17
skunkwerk
  • 2,920
  • 2
  • 37
  • 55
  • 2
    This is a static class variable, not an instance variable. Make an instance of the class first with a `self` variable, then copy it and you'll get your desired result on the `self` variable. As an aside, `deepcopy` is [super](https://stackoverflow.com/a/62865237/6243352) [slow](https://stackoverflow.com/questions/24756712/deepcopy-is-extremely-slow). Probably don't use it, or if you do, implement `__deepcopy__`. – ggorlen Oct 21 '20 at 05:16

2 Answers2

3

Your first question:

As pointed out by @Blckknght in the comments, when you pass a class-definition object to copy.deepcopy(), you run into a behavior of deepcopy() that is surprising / quirky, though intentional -- instead of making a deep copy of the class-definition object, deepcopy() merely returns a reference to the same class-definition object.

To verify this,

print ("b is:", b)
b_instance = b()
print ("b is Test ?:", (b is Test))

gives:

b is: <class '__main__.Test'>
b is Test ?: True             # deepcopy() hasn't made a copy at all !

Here's what the doc says regarding deepcopy() for class-definition objects:

This module does not copy types like module, method, stack trace, stack frame, file, socket, window, array, or any similar types. It does “copy” functions and classes (shallow and deeply), by returning the original object unchanged; this is compatible with the way these are treated by the pickle module.

So, when you do

b.a = 1

you're still modifying the class variable of the class Test.

Your second question:

When you do

c = type('Test2', (Test,), {'a': 2})

you are creating a new type called Test2, with its own class variable called a, and having Test as its superclass.

Now Test2.a and Test.a are two different class variables, defined in two different classes, and can take values independently of each other. One is a class variable called a in the superclass Test, the other is a class variable with the same name a in the subclass Test2.

That is why c.a will give 2 (the value to which you initialized it), and Test.a will continue to give 1

fountainhead
  • 3,584
  • 1
  • 8
  • 17
  • You might want your initial code block to test `b is Test`. The things you're currently doing don't conclusively show that `deepcopy` is a no-op for class objects. You sort of say that, but don't explain that this is somewhat unusual. Generally `deepcopy` *does* create a new object containing the same things, but it doesn't do so for classes (this is a design decision made by the developers of Python, not an absolute thing that couldn't have been designed another way). – Blckknght Oct 21 '20 at 05:58
  • @Blckknght, agree with your first point, and edited my answer, thanks. Your second point is that `deepcopy()` isn't doing a sincere copy in the case of class objects. I'm not sure about that. The observations that we're trying to rationalize here seem to indicate that `deepcopy()` **is** in fact doing **too good a job** of copying. So, instead of creating a class object for an entirely newly class, it is creating a class object that describes the exact same class `Test` -- which is what a proper copy-operation would be expected to do – fountainhead Oct 21 '20 at 06:10
  • 2
    There's no copy of `Test` being done at all. `b = copy.deepcopy(Test)` is equivalent to `b = Test`, it's just binding a new name to the existing class object. – Blckknght Oct 21 '20 at 06:16
  • @Blckknght, I get your point, updated my answer, thanks. – fountainhead Oct 21 '20 at 06:23
  • the term is just "class object" or "class", not "class definition object". – juanpa.arrivillaga Feb 15 '22 at 18:32
0

You copied the class definition and not the instance itself.

You should be doing something like this instead:

import copy

class Test:
    def __init__(self,a):
        self.a = a

test1 = Test(0)
test2 = copy.deepcopy(test1)

print("Original:")
print("Test 1: " + str(test1.a)) #0
print("Test 2: " + str(test2.a)) #0

print("Modify test 2 to 10")
test2.a = 10
print("Test 1: " + str(test1.a)) #0
print("Test 2: " + str(test2.a)) #10

Original:

Test 1: 0

Test 2: 0

Modify test 2 to 10

Test 1: 0

Test 2: 10

Jackson
  • 1,213
  • 1
  • 4
  • 14
  • thanks Jackson. Due to limitations of what I'm using this for (SQLAlchemy mixins), I had to copy the class itself, not the instance of the class (as that's not something my code itself handles). – skunkwerk Oct 21 '20 at 23:10