-2

I am trying to understand how class attributes work in Python. I have confusion based on following example.

#!/usr/bin/python3

class Bag:
    val = 100
    items = []

    def add(self, x):
        self.items.append(x)
        print(self.items)

b1 = Bag()
b1.add('book')
b1.val += 1
print(b1.val)

b1.add('pen')
b1.val += 1
print(b1.val)

b2 = Bag()
b2.add('sketches')
b2.val += 1
print(b2.val)

b2.add('text')
b2.val += 1
print(b2.val)

b2.add('canvas')
b2.val += 1
print(b2.val)

Output expected:

['book']
101
['book', 'pen']
102
['book', 'pen', 'sketches']
103
['book', 'pen', 'sketches', 'text']
104
['book', 'pen', 'sketches', 'text', 'canvas']
105

Output seen:

['book']
101
['book', 'pen']
102
['book', 'pen', 'sketches']
101
['book', 'pen', 'sketches', 'text']
102
['book', 'pen', 'sketches', 'text', 'canvas']
103

Why is there inconsistency between list being shared vs different copies of integers?

Here is another example that shows a different behavior for int.

#!/usr/bin/python3

class Person:
    cl_roll = 0

    def __init__(self, name):
        self.name = name
        self.roll = self.next_roll()
        print(self.roll, self.name)

    @classmethod
    def next_roll(cls):
        cls.cl_roll += 1
        return cls.cl_roll

p1 = Person('Eve')
p2 = Person('Abel')
p3 = Person('Eva')

Output expected based on previous output:

1 Eve
1 Abel
1 Eva

Actual output:

1 Eve
2 Abel
3 Eva
Quiescent
  • 1,088
  • 7
  • 18
  • 2
    Possible duplicate of [How to avoid having class data shared among instances?](https://stackoverflow.com/questions/1680528/how-to-avoid-having-class-data-shared-among-instances) – jonrsharpe Sep 12 '19 at 07:37
  • Thank you, will check the link. – Quiescent Sep 12 '19 at 07:39
  • It is still not clear to me. Any further explanation would be very helpful. – Quiescent Sep 12 '19 at 07:42
  • That's not a useful problem statement, *"Any further explanation"* could mean anything. What exactly is unclear? One important thing to note: integers are *immutable*. – jonrsharpe Sep 12 '19 at 07:43
  • The link discusses dictionary related issue and does not directly address this question. – Quiescent Sep 12 '19 at 08:31
  • What? The duplicate also talks about a list class attribute. That said, it it *was* a dictionary, which is also a mutable container, it would still apply. – jonrsharpe Sep 12 '19 at 08:32

2 Answers2

1

I will extend your example a bit

class Bag:
   items = []
   items1 = []
   val = 100
   def add(self, x):
       self.items.append(x)
       self.val += 1
       self.items1 += [x]


b1 = Bag()
print(Bag.__dict__)
#op-1>>> 'items': [], 'items1': [], 'val': 100,
print(b1.__dict__)
#op-2>>> {}

b1.add(111)
print(Bag.__dict__)
#op-3>>> {'items': [111], 'items1': [111], 'val': 100}
print(b1.__dict__)
#op-4>>>{'items1': [111], 'val': 101}

To go step-by-step:

  1. self.items.append(x):

    First, python tries to find if we have a items in the object (self.__dict__), if not then it tries to find items from the class scope and appends it. Nothing is returned as part of this expression. self.__dict__ is untouched after this expression.

  2. self.val += 1

    This is augmented assignment for int. So __iadd__ will be called, if this is not implemented 's __add__ will be called which will always return a new int. The old int is not changed in-place because it is immutable. To elaborate

    self.val = self.val + 1

    The first time self.val on the rhs refers to the class attribute (since b1.dict does not have it) and it creates a new int which is now stored in the object's __dict__ (because of lhs).. The second time self.val in the rhs refers to the val in self.__dict__ Instead if you would have done Bag.val += 1, then it will always manipulate the class variable (like your second example)

  3. self.items1 += [x]

So this is also augmented addition of the list.__iadd__. self.items1 is changed in-place for mutable sequences and the reference to the same list is also returned as part of this expression. So, after this statement, you should see that self.__dict__ will contain items1 but with same contents as Bag.__dict__['items1'].

Your second example is altogether different:

cls.cl_roll += 1 

this statement always manipulates the class variable.

Sam Daniel
  • 1,800
  • 12
  • 22
  • Thank you @sam-daniel! I had to lookup magic methods but it's worth it. IMHO, it would have been better if class variables are accessed/modified only using class methods. – Quiescent Sep 16 '19 at 06:07
0

The list items is one shared object among all the instances of your class. You modify it with self.items.append(x) but you don't ever create another list. So every reference to .items is working on the shared object Bag.items

However, when you do

b1.val += 1

This is like writing

b1.val = b1.val + 1

And because b1 does not yet have an individual value for val, the effect you get is:

b1.val = Bag.val + 1

You are assigning to b1.val a new value that is different from Bag.val.

So your instances have a shared items, but have individual val, because you actually assign to b1.val etc.

khelwood
  • 55,782
  • 14
  • 81
  • 108
  • Not really. I have examples to demonstrate the same. It can be reproduced this way. Create multiple objects in which only a few modify class variable. Then change class variable either using class/classmethod. Only some of them see modified values. – Quiescent Sep 12 '19 at 08:36