NB: Understanding Class Attributes

This notebook demonstrates how class and instance attributes are related to each other.

A Simple Example

We define a class with one attribute.

class Foo(): x = 1

We create an instance of the class.

foo1 = Foo()

Notice that the class defines the value for the instance.

foo1.x, Foo.x
(1, 1)

Now, the instance attribute changes if the class attribute is changed.

Foo.x = 2
foo1.x, Foo.x
(2, 2)

What if we the local attribute’s value?

What happens to the class attribute?

foo1.x = 3
foo1.x, Foo.x
(3, 2)

Turns out we cannot override a global with a local.

We can see that the instance attribute is now unaffected by changing the value of the global.

Foo.x = 4
foo1.x, Foo.x
(3, 4)

What happened?

By assigning a value to the instance attribute, we converted from global in the class to local in the instance.

This is similar to what we saw with local and global variables in functions.

Finally, notice how changing the value of the class attribute changes all the instance attributes that have not overridden the attribute.

foo2 = Foo()
foo3 = Foo()
Foo.x = 10
foo1.x, foo2.x, foo3.x, Foo.x
(3, 10, 10, 10)

Mutable Class Attributes

There is an interesting gotcha regarding class attributes in Python.

Lists and other mutable data structures can be class attributes and yet have their values modified by instances.

This is kind of weird, and you should look out for it.

To demonstrate, we define a class with two instance variables, one a scalar and one a list.

We define a method to alter the value of each.

We also define a method compare the state of the instance with that of its class.

class MyTest():
    
    # Two class attributes
    foo = 0  
    bar = [] 
    
    def add_one(self):
        "A method to alter the values of the class attributes."
        self.foo += 1       
        self.bar.append(1)  
        
    def replace_bar(self, new_list = []):
        "A method to redefined the class list attribute."
        self.bar = new_list 
        
    def compare_states(self):
        "A method to compare the state of instance to that of its class."
        print('i.foo =', self.foo)
        print('c.foo =', __class__.foo) # Notice how we can refer to an instance's class
        print('i.bar =', self.bar)
        print('c.bar =', __class__.bar)
        

Now let’s run some tests.

We define and instance and change nothing.

test1 = MyTest()
test1.compare_states()
i.foo = 0
c.foo = 0
i.bar = []
c.bar = []

Now let’s increment the attributes and see the results.

test1.add_one()
test1.compare_states()
i.foo = 1
c.foo = 0
i.bar = [1]
c.bar = [1]

The method does disconnect the instance foo from the class foo.

But it does not disconnect the instance bar from the class bar.

Instead, a change that took place in one instance affects the state of all other instances!

The difference is that foo is a scalar, and bar is a list, i.e. mutable data structure.

We do it again to drive the point home.

test1.add_one()
test1.compare_states()
i.foo = 2
c.foo = 0
i.bar = [1, 1]
c.bar = [1, 1]

Now, let’s replace list itself in the instance.

test1.replace_bar()
for i in range(5):
    print("Iter", i)
    test1.add_one()
    test1.compare_states()
    print()
Iter 0
i.foo = 3
c.foo = 0
i.bar = [1]
c.bar = [1, 1]

Iter 1
i.foo = 4
c.foo = 0
i.bar = [1, 1]
c.bar = [1, 1]

Iter 2
i.foo = 5
c.foo = 0
i.bar = [1, 1, 1]
c.bar = [1, 1]

Iter 3
i.foo = 6
c.foo = 0
i.bar = [1, 1, 1, 1]
c.bar = [1, 1]

Iter 4
i.foo = 7
c.foo = 0
i.bar = [1, 1, 1, 1, 1]
c.bar = [1, 1]

Notice that now the class list is not altered by the instance list.

It remains in the state before the list itself was re-assigned by the instance.

This is because we redefined the list itself, not just its content.

Let’s define a second instance.

test2 = MyTest()
test2.compare_states()
i.foo = 0
c.foo = 0
i.bar = [1, 1]
c.bar = [1, 1]

The new instance has the original value of foo.

However, notice it starts of with the modified value of bar before it was replaced.

We do it a few more times to drive the point home.

for i in range(5):
    print("Iter", i)
    test2.add_one()
    test2.compare_states()
    print()
Iter 0
i.foo = 1
c.foo = 0
i.bar = [1, 1, 1]
c.bar = [1, 1, 1]

Iter 1
i.foo = 2
c.foo = 0
i.bar = [1, 1, 1, 1]
c.bar = [1, 1, 1, 1]

Iter 2
i.foo = 3
c.foo = 0
i.bar = [1, 1, 1, 1, 1]
c.bar = [1, 1, 1, 1, 1]

Iter 3
i.foo = 4
c.foo = 0
i.bar = [1, 1, 1, 1, 1, 1]
c.bar = [1, 1, 1, 1, 1, 1]

Iter 4
i.foo = 5
c.foo = 0
i.bar = [1, 1, 1, 1, 1, 1, 1]
c.bar = [1, 1, 1, 1, 1, 1, 1]

Some take-aways:

  • Class attribute changes affect those attributes in all of it instances …
  • … unless the instance assigns a value to the attribute.
  • However, appending to a list — or, more generally, modifying data in a mutable data structure — does not count as an assignment operation. The instance changes will affect the class state.
  • Bottom line: DEFINE CLASS ATTRIBUTES WITH CAUTION.