We have seen how to leverage the differences between mutable and immutable objects and what happens when you use mutable types as default function arguments. However, we haven't discussed what happens when you use mutable types as default attributes of classes.
Default values for attributes can be defined in different ways in your
classes. Let's start by looking at what happens if you define them in
the __init__
method. Let's start with a simple class that takes one
list as the argument when instantiating:
class MyClass:
def __init__(self, var=[]):
self.var = var
def append(self, value):
self.var.append(value)
def __str__(self):
return str(self.var)
This is a very simple example that already will show a very peculiar
behavior. The __init__
takes one list as the argument, and if it is
not provided it will use an empty list as default. We have also added a
method for appending values to the list. The __str__
method was
defined for convenience to explore the contents of the var
attribute.
We can instantiate the class and use it as always:
my_class = MyClass()
print(my_class)
# []
my_class.append(1)
print(my_class)
# [1]
So far so good, but let's see what happens when we instantiate the second class:
my_class_2 = MyClass()
print(my_class_2)
# [1]
The second time you instantiate a class, it will use a different default value! It is actually using the updated value from the first instance. Moreover, if you change the value of the second instance, the value of the first instance will also change:
my_class_2.append(2)
print(my_class)
# [1, 2]
Whatever changes you do to the attribute var
of one of the objects,
will be reflected into the other. Both attributes are actually the same
object, as you can verify by looking at their ids:
print(id(my_class.var))
# 140228152031752
print(id(my_class_2.var))
# 140228152031752
But the two instances are different
print(id(my_class))
# 140228175513360
print(id(my_class_2))
# 140228175513304
The same pattern that appeared while using mutable variables as defaults with functions will appear when using mutable default arguments of methods in custom classes. If you want to avoid this from happening, you can always check what we have done when working with functions.
Of course, the same pattern will appear if you use a mutable variable defined outside of the class, for example:
my_list = [1, 2, 3]
my_class = MyClass(my_list)
my_class.append(4)
print(my_list)
# [1, 2, 3, 4]
Classes provide another pattern which is the use of class attributes
instead of instance attributes. Class attributes are those values
that are defined directly in the class, outside of any methods. Let's
update our example to use a class attribute called var
:
class MyClass:
var = []
def append(self, value):
self.var.append(value)
def __str__(self):
return str(self.var)
And we use it as before:
my_class = MyClass()
my_class.append(1)
print(my_class)
# [1]
If we instantiate the class again, we will have the same as before:
my_class_2 = MyClass()
print(my_class_2)
# [1]
The main difference with what we have done before is that we can address
directly the var
attribute of the class:
MyClass.var.append(2)
print(my_class)
# [1, 2]
print(my_class_2)
# [1, 2]
You can also address the attribute of an instance directly, without the
need of the append
method:
my_class_2.var += [3]
print(my_class)
# [1, 2, 3]
print(my_class_2)
# [1, 2, 3]
You can see in the examples above, is that the changes you apply to one
of the attributes will be reflected in the attributes of all the other
instances and even in the class itself. There is a big difference,
however, between class attributes and default inputs in methods. Class
attributes are shared between instances by default even if they are
immutable. Let's see, for example, what happens if we use a var
that
is an integer, and therefore immutable:
class MyClass:
var = 1
def increase(self):
self.var += 1
def __str__(self):
return str(self.var)
Just as we have done before, we will instantiate twice the class and see what happens:
my_class = MyClass()
print(my_class)
# 1
my_class_2 = MyClass()
print(my_class_2)
# 1
my_class.increase()
print(my_class)
# 2
print(my_class_2)
# 1
What you see here is already a big difference. Both instances of
MyClass
have the same attribute var
. However, when you increase the
value in one of the instances this change is not propagated to the other
instance nor to new instances of the class.
This is very different from what you would see if you change the value
of var
in the class itself:
my_class = MyClass()
my_class_2 = MyClass()
MyClass.var += 1
print(my_class)
# 2
print(my_class_2)
# 2
You see that class attributes are still linked to the instances. It is
very interesting to see the id of the var
attribute before and after
changing its value:
my_class = MyClass()
my_class_2 = MyClass()
print(id(my_class_2.var))
# 10935488
print(id(my_class.var))
# 10935488
print(id(MyClass.var))
# 10935488
MyClass.var += 1
print(id(my_class_2.var))
# 10935520
print(id(my_class.var))
# 10935520
print(id(MyClass.var))
# 10935520
You see that all the attributes are the same object. When the value is
replaced, since integers are immutable, a new object is created and is
propagated to all the instances of the class. However, if you change the
value of var
in one of the instances, this will not hold anymore:
my_class.var += 1
print(id(my_class.var))
# 10935552
print(id(my_class_2.var))
# 10935520
print(id(MyClass.var))
# 10935520
You can see that both the attributes in MyClass
and in my_class_2
are still the same object, while the identity of var
in my_class
changed. From now on, any changes that you do to MyClass.var
are
decoupled from the changes in my_class
, but will still be reflected on
my_class_2
.
Keeping in mind the differences between methods' default values and class attributes open a lot of possibilities when designing a program. The fact that you can alter all objects from within a specific instance can be of great use when properties change at runtime. Even if not an extremely common scenario for short-lived scripts, it is very common when dealing with user interaction on programs that run for hours or days.
Header photo by Dan Gold on Unsplash