Reputation: 6028
New python users often get tripped up by mutable argument defaults. What are the gotchas and other issues of using this 'feature' on purpose, for example, to get tweakable defaults at runtime that continue to display properly in function signatures via help()
?
class MutableString (str):
def __init__ (self, value):
self.value = value
def __str__ (self):
return self.value
def __repr__ (self):
return "'" + self.value + "'"
defaultAnimal = MutableString('elephant')
def getAnimal (species=defaultAnimal):
'Return the given animal, or the mutable default.'
return species
And in use:
>>> help(getAnimal)
getAnimal(species='elephant')
Return the given animal, or the mutable default.
>>> print getAnimal()
elephant
>>> defaultAnimal.value = 'kangaroo'
>>> help(getAnimal)
getAnimal(species='kangaroo')
Return the given animal, or the mutable default.
>>> print getAnimal()
kangaroo
Upvotes: 3
Views: 144
Reputation: 365767
First, read Why are default values shared between objects. That doesn't answer your question, but it provides some background.
There are different valid uses for this feature, but they pretty much all share something in common: the default value is a transparent, simple, obviously-mutable, built-in type. Memoization caches, accumulators for recursive calls, optional output variables, etc. all look like this. So, experienced Python developers will usually spot one of these use cases—if they see memocache={}
or accum=[]
, they'll know what to expect. But your code will not look like a use for mutable default values at all, which will be as misleading to experts as it is to novices.
Another problem is that your function looks like it's returning a string, but it's lying:
>>> print getAnimal()
kangaroo
>>> print getAnimal()[0]
e
Of course the problem here is that you've implemented MutableString
wrong, not that it's impossible to implement… but still, this should show why trying to "trick" the interpreter and your users tends to open the door to unexpected bugs.
--
The obvious way to handle it is to store the changing default in a module, function, or (if it's a method) instance attribute, and use None
as a default value. Or, if None
is a valid value, use some other sentinel:
defaultAnimal = 'elephant'
def getAnimal (species=None):
if species is None:
return defaultAnimal
return species
Note that this is pretty much exactly what the FAQ suggests. Even if you inherently have a mutable value, you should do this dance to get around the problem. So you definitely shouldn't create a mutable value out of an inherently immutable one to create the problem.
Yes, this means that help(getAnimal)
doesn't show the current default. But nobody will expect it to.
They will probably expect you to tell them that the default value is a hook, of course, but that's a job for a docstring:
defaultAnimal = 'elephant'
def getAnimal (species=None):
"""getAnimal([species]) -> species
If the optional species parameter is left off, a default animal will be
returned. Normally this is 'elephant', but this can be configured by setting
foo.defaultAnimal to another value.
"""
if species is None:
return defaultAnimal
return species
Upvotes: 4
Reputation: 375535
The only useful use I've seen for it is as a cache:
def fibo(n, cache={}):
if n < 2:
return 1
else:
if n in cache:
return cache[n]
else:
fibo_n = fibo(n-1) + fibo(n-2) # you can still hit maximum recursion depth
cache[n] = fibo_n
return fibo_n
...but then it's cleaner to use the @lru_cache
decorator.
@lru_cache
def fibo(n):
if n < 2:
return 1
else:
return fibo(n-1) + fibo(n-2)
Upvotes: 2