1

I do not fully understand, what the difference of class attribute integer or list in the following example is:

class MyClass(object):

    a = [1]
    b = 1

    def __init__(self):
        print(self.a, id(MyClass.a))
        print(self.b, id(MyClass.b))

    def modify(self):
        self.a.append(2)
        print("modfied list attribute:", MyClass.a, id(MyClass.a))
        print("modfied listattribute:", self.a, id(self.a))

        self.b += 1
        print("modfied int attribute:", MyClass.b, id(MyClass.b))
        print("modfied int attribute:", self.b, id(self.b))

x = MyClass()
x.modify()
y = MyClass()

In the output of this example one can see, that if I alter the list in one instance (x), the actual atribute in MyClass is altered. The integer however when changed in one instance remains for the class the same. I assume, that this has to do with features of a list, i.e. that the id remains if some value is appended.

Can someone give me a short explanation in a nutshell, what the main reason behind this behaviour is?

Sosel
  • 1,678
  • 1
  • 16
  • 31
  • Appending to `a` is modifying a shared object. Whereas `self.b += 1` is assigning a different value to the instance variable `b` for one instance. – khelwood Oct 08 '19 at 14:36
  • Possible duplicate of [Python class attribute inconsistency](https://stackoverflow.com/questions/57901837/python-class-attribute-inconsistency/57902524) – khelwood Oct 08 '19 at 14:37

2 Answers2

3

int values are immutable. You didn't not modify an existing int, but replaced self.b with a different int value.

list values are mutable, and list.append modifies the instance that invokes it. self.a is modified in-place without replacing the existing list with a new one.

Basically, you cannot predict the behavior of += without knowing the type of the value using it. In b = 0; b += 1, b refers to a new instance of int. But in b = []; b += [2,3], the existing list is extended (as if you had called b.extend([2,3])).

chepner
  • 497,756
  • 71
  • 530
  • 681
  • So I understand: when creating multiple instances of one class, all list attributes as initialised above will point on the same intance of this list. Thanks for this clear explanation! – Sosel Oct 08 '19 at 14:43
  • 1
    To see it from another perspective, a and b are pointing to two different objects. Variable 'a' points to a list, you modify that list, so 'a' is modified. Variable 'b' points to an int, you can't modify that int, so it gives you the original value again. – micric Oct 08 '19 at 14:43
  • Also, if you want each instance to have its own list, put `self.a = [1]` inside `__init__` instead of having each instance share a single class attribute. – chepner Oct 08 '19 at 14:44
  • I agree, within init would be better in this specific case. Or, in my case, I would rather use a set instead of a list, since a set as class attribute also should be immutalble. – Sosel Oct 08 '19 at 14:46
  • Yes, I will add that unless you know *exactly what you're doing* (I'm not saying there's no good case for this; sometimes there is) it's not often a good idea to use a list as a class attribute, as it can lead to some odd memory leaks and the like. – Iguananaut Oct 08 '19 at 14:49
  • 1
    No, a `set` is also mutable. There is an immutable variant, `frozenset`. `tuple` is the closest thing to an immutable list. – chepner Oct 08 '19 at 14:49
  • Ahh, exactly, tuple is the weapon of choice... thanks. – Sosel Oct 08 '19 at 14:51
  • @Sosel that is **always how class attributes work**, *regardless of the type*. – juanpa.arrivillaga Oct 08 '19 at 15:58
0

See how you can make it visual with the is statement (NOT with ==):

class MyClass(object):

    a = [1]
    b = 1

    def __init__(self):
        print(self.a, id(MyClass.a))
        print(self.b, id(MyClass.b))

    def modify(self):
        self.a.append(2)
        print("modfied list attribute:", MyClass.a, id(MyClass.a))
        print("modfied listattribute:", self.a, id(self.a))

        self.b += 1
        print("modfied int attribute:", MyClass.b, id(MyClass.b))
        print("modfied int attribute:", self.b, id(self.b))

x = MyClass()
y = MyClass()
print(x.a is y.a)  # True
print(x.b is y.b)  # True

x.modify()

print(x.a is y.a)  # True
print(x.b is y.b)  # Falseclass MyClass(object):

    a = [1]
    b = 1

    def __init__(self):
        print(self.a, id(MyClass.a))
        print(self.b, id(MyClass.b))

    def modify(self):
        self.a.append(2)
        print("modfied list attribute:", MyClass.a, id(MyClass.a))
        print("modfied listattribute:", self.a, id(self.a))

        self.b += 1
        print("modfied int attribute:", MyClass.b, id(MyClass.b))
        print("modfied int attribute:", self.b, id(self.b))

x = MyClass()
y = MyClass()

# Before changing something a and b is a reference to the same object
print(x.a is y.a)  # True
print(x.b is y.b)  # True

x.modify()

# After that only x.b is a reference to a new object
# you accessed the list object that is shared and append a value
print(x.a is y.a)  # True
print(x.b is y.b)  # False

# If you would give x.a a new list it would look like this
x.a = []
print(x.a is y.a)  # False
print(x.b is y.b)  # False
# x.a is now not the same list as y.a

Unless you have a reason to use a and b as class attribute i would suggest you to make it instance attributes:

class MyClass(object):

    def __init__(self):
        self.a = [1]
        self.b = 1
        print(self.a, id(self.a))
        print(self.b, id(self.b))

    def modify(self):
        self.a.append(2)
        print("modfied list attribute:", self.a, id(self.a))
        print("modfied listattribute:", self.a, id(self.a))

        self.b += 1
        print("modfied int attribute:", self.b, id(self.b))
        print("modfied int attribute:", self.b, id(self.b))

x = MyClass()
y = MyClass()
print(x.a is y.a)  # False
print(x.b is y.b)  # False

x.modify()

print(x.a is y.a)  # False
print(x.b is y.b)  # False


x.a = []
print(x.a is y.a)  # False
print(x.b is y.b)  # False
Frank
  • 1,959
  • 12
  • 27