class Foo(): x = 1
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.
Then we create an instance of the class.
= Foo() foo1
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.
= 2 Foo.x
foo1.x, Foo.x
(2, 2)
What if we change the local attribute’s value?
What happens to the class attribute?
= 3 foo1.x
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.
= 4 Foo.x
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.
= Foo()
foo2 = Foo() foo3
= 10 Foo.x
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
= 0
foo = []
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.
= MyTest() test1
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.
= MyTest()
test2 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.