NB: Understanding Class Attributes

Programming for Data Science

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

Then 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)

Note also that the instance attribute changes if the class attribute is changed.

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

What if we change 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 the class attribute with the instance.

We can also see that the instance attribute is now unaffected by changing the value in the class.

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 redefine 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('instance.foo =', self.foo)
        print('class.foo =', __class__.foo) # Notice how we can refer to an instance's class
        print('instance.bar =', self.bar)
        print('class.bar =', __class__.bar)
        

Now let’s run some tests.

We define an instance and change nothing.

test1 = MyTest()
test1.compare_states()
instance.foo = 0
class.foo = 0
instance.bar = []
class.bar = []

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

test1.add_one()
test1.compare_states()
instance.foo = 1
class.foo = 0
instance.bar = [1]
class.bar = [1]

The method .add_one() 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.

When the instance mutates the class attribute, the class attribute is not reassigned — only its contents change.

We do it again to drive the point home.

test1.add_one()
test1.compare_states()
instance.foo = 2
class.foo = 0
instance.bar = [1, 1]
class.bar = [1, 1]

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

test1.replace_bar()
# test1.bar = [] # Same as

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

Iter 1
instance.foo = 4
class.foo = 0
instance.bar = [1, 1]
class.bar = [1, 1]

Iter 2
instance.foo = 5
class.foo = 0
instance.bar = [1, 1, 1]
class.bar = [1, 1]

Iter 3
instance.foo = 6
class.foo = 0
instance.bar = [1, 1, 1, 1]
class.bar = [1, 1]

Iter 4
instance.foo = 7
class.foo = 0
instance.bar = [1, 1, 1, 1, 1]
class.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()
instance.foo = 0
class.foo = 0
instance.bar = [1, 1]
class.bar = [1, 1]

The new instance has the original value of foo.

However, notice it starts off 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
instance.foo = 1
class.foo = 0
instance.bar = [1, 1, 1]
class.bar = [1, 1, 1]

Iter 1
instance.foo = 2
class.foo = 0
instance.bar = [1, 1, 1, 1]
class.bar = [1, 1, 1, 1]

Iter 2
instance.foo = 3
class.foo = 0
instance.bar = [1, 1, 1, 1, 1]
class.bar = [1, 1, 1, 1, 1]

Iter 3
instance.foo = 4
class.foo = 0
instance.bar = [1, 1, 1, 1, 1, 1]
class.bar = [1, 1, 1, 1, 1, 1]

Iter 4
instance.foo = 5
class.foo = 0
instance.bar = [1, 1, 1, 1, 1, 1, 1]
class.bar = [1, 1, 1, 1, 1, 1, 1]

Some Observations

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 from an instance — 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: use class attributes with caution.