Gary Fixler
Gary Fixler

Reputation: 6028

Is taking advantage of the one-time binding of function arguments a bad idea?

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

Answers (2)

abarnert
abarnert

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

Andy Hayden
Andy Hayden

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

Related Questions