Tags Python
I've just added a new gotcha to my Python insights page, and I want to reproduce it here because IMHO it's very important:

This may surprise you:

>>> class Foo(object):
...     def __init__(self, name='', stuff=[]):
...         self.name = name
...         self.stuff = stuff
...
...     def add_stuff(self, gadget):
...         self.stuff.append(gadget)
...
>>> f = Foo()
>>> f.add_stuff('tree')
>>> f.stuff
['tree']
>>> g = Foo()
>>> g.stuff
['tree']

Where did this tree come from in g??

This is something that confuses a lot of Python programmers, sometimes even the experienced ones. Almost every newbie is hit by this gotcha at one stage or another (unless he diligently read about it in someone else's blog, book or Python recipe). What happens here is that default values for arguments are created by Python only once per each function/method, at the time of its definition.

We can easily verify it by checking the addresses:

>>> object.__repr__(f.stuff)
'<list object at 0x01B35828>'
>>> object.__repr__(g.stuff)
'<list object at 0x01B35828>'

So how can one do it correctly? One solution is avoid using mutable default values for arguments. But this is hardly satisfactory, as from time to time a new list is a useful default. There are some complex solutions like defining a decorator for functions that deep-copies all arguments. This is an overkill, and the problem can be solved easily as follows:

>>> class Foo(object):
...     def __init__(self, name='', stuff=[]):
...         self.name = name
...         self.stuff = stuff or []
...
...     def add_stuff(self, gadget):
...         self.stuff.append(gadget)
...
>>> f = Foo()
>>> f.add_stuff('tree')
>>> g = Foo()
>>> g.stuff
[]
>>> f.stuff
['tree']

The stuff or [] code does the trick, as the [] in it always creates a fresh new list when the empty list (that is, the default argument) is passed in.

Note that the or operator is quite nondiscriminatory when it comes to booleans, so a lot of values you may consider valid (empty strings, 0, etc.) will be thought of as False. But for most cases this will work just fine (why would anyone pass an empty string instead of a list?)

However, if you're still concerned of the tricky corner cases, this solution may be more robust (though less simple, which is a disadvantage):

>>> class Foo(object):
...     def __init__(self, name='', stuff=None):
...         self.name = name
...         if stuff is None: stuff = []
...         self.stuff = stuff
...
...     def add_stuff(self, gadget):
...         self.stuff.append(gadget)

Comments

comments powered by Disqus