Mutable vs. Immutable Objects


DesertPy Lightning Talk, June 2015


Ashley Anderson

@aganders3 on Twitter or [email protected]

Everything is an object

Every Python object has a type, identity, and value.

The builtin function type() returns a type object.
The builtin function id() returns the object's identity (an integer).
Value is kind of vague, though.

In [169]:
print 1, type(1), id(1)

foo = range(10)
print foo, type(foo), id(foo)

import sys   
print sys, type(sys), id(sys)

def bar():
    pass

print bar, type(bar), id(bar)
1 <type 'int'> 4299197592
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] <type 'list'> 4415085312
<module 'sys' (built-in)> <type 'module'> 4297509808
<function bar at 0x10728b320> <type 'function'> 4415075104

Mutability is about values

Think of an object's value as any and all of its attributes. For example, anything you'd type self. to get at from inside if you wrote the class yourself.

Most custom classes are mutable. To create a simple immutable class, try using a namedtuple. Otherwise you can raise a TypeError from __setattr__ to make your own immutable type.

Immutable objects

You can't change their values.
The value of x is the same before and after foo is called.

x = some_immutable
foo(x)
print x

This is kind of like pass-by-value.

Immutable objects include numbers, strings, tuples.

In [120]:
def add_one(bar):
    bar += 1

b = 5
print b, id(b)
add_one(b)
print b, id(b)
5 4299197496
5 4299197496

Mutable objects

You can change their values.
The value of y may be different after foo is called.

y = some_mutable
foo(y)
print y

This is kind of like pass-by-reference.

Mutable objects include lists, dictionaries, etc.

In [121]:
def reverse_list(foo):
    foo.reverse()

a = range(10)
print a, id(a)
reverse_list(a)
print a, id(a)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 4415096664
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0] 4415096664

But, wait...

It's pretty easy to change the value of a number variable...

In [151]:
foo = 42
print foo

foo += 1
print foo

foo = 42
print foo
42
43
42

Nah...

The assignment operator doesn't change the value of an object, it changes its identity.

In [148]:
foo = 42
print foo, id(foo)
foo += 1
print foo, id(foo)
foo = "bar"
print foo, id(foo)
42 4299198584
43 4299198560
bar 4381029040

Values get confusing when we have containers

If you have an immutable container (e.g. a tuple) that holds mutable objects (e.g. lists), you can still mutate the contained objects without mutating the container.

In [171]:
A = ([],[],[])
print A, id(A[0]), id(A)
A[0].append(5)
print A, id(A[0]), id(A)
A[1] = [5]
([], [], []) 4414903504 4414893984
([5], [], []) 4414903504 4414893984
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-171-868c79a8f9fc> in <module>()
      3 A[0].append(5)
      4 print A, id(A[0]), id(A)
----> 5 A[1] = [5]

TypeError: 'tuple' object does not support item assignment

Gotcha!

Mostly you don't have to think about this stuff, but sometimes you do.

In [1]:
def my_appending_function(element, initial_list=[]):
    # do some stuff
    initial_list.append(element)
    return initial_list
In [2]:
print my_appending_function((1,2,3,5), range(2))
print my_appending_function(10)
print my_appending_function(5)
[0, 1, (1, 2, 3, 5)]
[10]
[10, 5]

Mutable objects as default arguments

These are defined once when the function is defined. This is a result of def being an executable statement that binds the name of the function to the function object. All this has something to do with Python functions being "first class" objects.

Mutable objects as deafault arguments

So...instead of this:

In [165]:
def my_appending_function(element, initial_list=[]):
    # do some stuff
    initial_list.append(element)
    return initial_list

Do this:

In [166]:
def my_appending_function(element, initial_list=None):
    if initial_list is None:
        initial_list = []
    # do some stuff
    initial_list.append(element)
    return initial_list

Mutable objects as default arguments

Now it does what you (may have) expected:

In [77]:
def my_appending_function(number, initial_list=None):
    if initial_list is None:
        initial_list = []
    # do some stuff
    initial_list.append(number)
    return initial_list
In [167]:
print my_appending_function(10)
print my_appending_function(5)
print my_appending_function(10)
print my_appending_function(5)
[10]
[5]
[10]
[5]