3

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
Gary Fixler
  • 5,632
  • 2
  • 23
  • 39
  • 2
    Don't do it. It trips over experienced programmers as well. – Simeon Visser May 17 '13 at 22:32
  • 1
    Have you read [Why are default values shared between objects?](http://docs.python.org/3.3/faq/design.html#why-are-default-values-shared-between-objects) in the official FAQ? (It doesn't answer your whole question, but you definitely should read it first.) – abarnert May 17 '13 at 23:23
  • I've read it, and many more things like it. I understand how the defaults are bound at definition time, and that they are hence referenced. What I'm asking regards knowing how it works, and seeking to exploit it, and wondering about unforseen gotchas (specifics, not "don't do it!"). I don't personally see the issue with the above, as it's just changing the default value used when the function is called. – Gary Fixler May 18 '13 at 00:30
  • 1
    They can occasionally be [useful](http://stackoverflow.com/a/4115934/355230). – martineau May 18 '13 at 00:55
  • @martineau: That seems like just a special case of the memoization cache, or maybe they're both special cases of the more general "static variable"-in-the-C-function-static-sense case. – abarnert May 18 '13 at 01:00
  • @abarnert: Generally speaking, the latter. Often it's cleaner and less work than using function attributes. – martineau May 18 '13 at 01:38
  • @martineau: Yeah, you're right; in fact, _most_ uses for mutable defaults in Python are special cases of one larger case, which maybe I should have explained better in my answer. Static variables in C, let or similar extra scopes in many functional languages, bind(this) in JS, etc. are all parallel idioms, but none are _exactly_ parallel. – abarnert May 20 '13 at 19:43

2 Answers2

4

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
abarnert
  • 354,177
  • 51
  • 601
  • 671
  • 1
    "transparent, simple, obviously-mutable" = spot on :) – Andy Hayden May 17 '13 at 23:49
  • "Obviously-mutable" is a really great consideration, and the kind of thing I was hoping to uncover with my question. My example is non-obvious, though I was more interested in use-cases in class methods. For example, consider a `Grid.create(spacing=[20,40])`. I could leave that option to `__init__`, and provide a `setSpacing` method, and not allow passing it to `create`, or hide it in `**kwargs`, but it seemed "transparent, simple, and obvious" to allow setting it, then seeing what the current values are with `help(myGrid.create)`. I could always get around it with a properties view method. – Gary Fixler May 18 '13 at 00:38
  • Also great point about the unexpected bugs, and devs' expectations, but I casually rebel against a few things here. For example, I don't consider `arg=None` to be a good thing; it requires reading code instead of function signatures to understand what are really quite simple things. – Gary Fixler May 18 '13 at 00:46
  • I also don't like docstrings, almost as much as I don't like comments. They get out of sync constantly, and I have hundreds of examples of this in various code repositories from a handful of companies. I'm a real stickler for dotting every i/crossing every t, and even I still have a pile of outdated comments and docstrings littering my history. I've lost time following docstrings full of lies for hours on end. – Gary Fixler May 18 '13 at 00:47
  • I'll add that I can accept that "it is how it is and we have to deal with it" as answers for things (life's rough), but not "the best way to do it is [not very good way]," unless we're talking "the best way given constraints of the system," as opposed to "the best way in theory." – Gary Fixler May 18 '13 at 00:48
  • You have made good points about expectations and at least one gotcha (not really a string; can't index), and I concede those. However, you've also pointed out cases where it's acceptable to bend the rules (memocache and accum), because they're obvious. I was thinking more along the lines of a class method like this: `def foo (bar=Mutable('baz'))`, which would be obvious, and would make more sense given the state in a class. In fact, I could see a metaclass or a decorator providing this kind of ability readily; specify an argument as mutable, and it becomes an instance-accessible 'setting.' – Gary Fixler May 18 '13 at 00:52
  • @GaryFixler: I think you're missing the main point. What you want to do is _not pythonic_. It's not impossible, or inefficient, or anything like that. The reason it's bad is that it's not the way the Python community does things. It will be harder, and less pleasant, for others to contribute to your project, take over your code, answer your questions on SO, etc. If the stdlib, the FAQ, the tutorial, most major projects online, etc. all do something one way, you _can_ do things differently, but do you really want to? – abarnert May 18 '13 at 00:57
  • @GaryFixler: … Especially since it seems like the major thing you're trying to gain here is improved `help` output, which implies that you care pretty strongly about your code being readable/usable by others. – abarnert May 18 '13 at 00:58
  • It's my non-conformist nature. "Non-pythonic" seems at times to mean little more than "not part of the zeitgeist." These explorations sometimes pan out. They're how languages (and zeitgeists) grow, evolve, and why new languages occasionally come along. Sometimes these wishes have spawned PEPs, and made it into the language. A few things people touted in Python 2.x are now decried by 3.x apologists. `arg=None` is canon, but doesn't seem too well-loved. At any rate, you've made very good points, all of which I accept, and as such, I will accept your answer. Thanks! – Gary Fixler May 18 '13 at 03:01
  • @GaryFixler: You're right, your question definitely could be seen or used as a way to help solidly define (by challenging) a Python idiom that we all loosely know but rarely think about. And that could lead to discovering that the idiom was more powerful than we expected. (As with Greg Ewing's explorations of `yield from`. It was meant as a shorthand way to delegate an iterator, but he realized it could be use to build the coroutines he wanted without the explicit coroutine support that the core devs refused to build… and now Guido is writing PEP 3156 around `yield from` coroutines.) – abarnert May 20 '13 at 19:50
2

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)
Community
  • 1
  • 1
Andy Hayden
  • 359,921
  • 101
  • 625
  • 535
  • 1
    That's not the _only_ use. For example, it's the obvious way to pass down an accumulator for downward-style recursion. Also, it doesn't directly answer his question. But it definitely is a useful illustration. – abarnert May 17 '13 at 23:44