How does mutability in python work? What can happen when mutability goes wrong?
This talk covers mutability in Python and how it can cause common bugs in Python. We cover some terrible (or "evil") mutable code. We then cover some safer, if not still questionable, "good" mutable code to show how it could be used to create powerful features in Python.
5. What is mutability
A mutable object is an object which can be changed “in-place”.
Changing the state of a mutable object updates all variables referencing it.
An immutable object cannot be changed “in-place”.
Changing an immutable object creates a new object for a single variable.
12. What is the issue with mutability?
Mutability is simple on the surface.
However it is a powerful feature of Python that can be misleading.
Without understanding it properly it can lead to strange bugs.
16. So we just need to watch out for it?
Yes and no.
Understanding and recognising mutability is very important.
Making it obvious is a good way of doing this.
17. Which is more obvious?
state = []
def append_data(data):
state.append(data)
if __name__ == ‘__main__’:
append_data(1)
state = []
def append_data(data, state):
state.append(data)
return state
if __name__ == ‘__main__’:
state = append_data(1, state)
18. Which is more obvious?
class StatefulClass:
def __init__(self, data):
self.data = data
self.state = []
def append_data(self):
self.state.append(self.data)
def run(self):
self.append_data()
class StatefulClass:
def __init__(self, data):
self.data = data
self.state = []
def append_data(self, state):
state.append(self.data)
return state
def run(self):
self.state = self.append_data(self.state)
20. Mutable default Arguments
Mutable default arguments are a common GOTCHA in Python as they can change
the expected output of a function in misleading ways.
def func(a, b=[]):
b.append(a)
return b
>>> my_list = func(1)
>>> my_list
[1]
>>> my_new_list = func(1)
>>> my_new_list
[1, 1]
21. A possible use case for mutable default arguments?
Solving a circular dependency...
# a.py
from b import b1
def a1():
print('hi')
def a2():
b1()
if __name__ == '__main__':
a2()
# b.py
from a import a1
def b1():
a1()
print('there')
if __name__ == '__main__':
b1()
22. A possible use case for mutable default arguments?
Solving a circular dependency...
# a.py
from b import b1
def a1():
print('hi')
def a2():
b1(a1)
b1()
if __name__ == '__main__':
a2()
# b.py
def b1(func=None, callables=[]):
if func is not None:
callables.append(func)
return
for callable in callables:
callable()
print('there')
if __name__ == '__main__':
b1()
25. Is there a good use for mutable default arguments?
Mutable default arguments can solve a lot of problems.
But they are rarely the right way to solve those problems.
Some arguments for memoization and binding in local scope.
Avoid if possible.
32. So in conclusion
Avoid shared references.
Avoid default mutable arguments.
But don’t avoid mutability in nonlocal scope.
Keep your code obvious.
Don’t overcomplicate things.