views:

536

answers:

8

Now following my series of "python newbie questions" and based on another question.

Prerogative

Go to http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html#other-languages-have-variables and scroll down to "Default Parameter Values". There you can find the following:

def bad_append(new_item, a_list=[]):
    a_list.append(new_item)
    return a_list

def good_append(new_item, a_list=None):
    if a_list is None:
        a_list = []
    a_list.append(new_item)
    return a_list

There's even an "Important warning" on python.org with this very same example, tho not really saying it's "better".

One way to put it

So, question here is: why is the "good" syntax over a known issue ugly like that in a programming language that promotes "elegant syntax" and "easy-to-use"?

edit:

Another way to put it

I'm not asking why or how it happens (thanks Mark for the link).

I'm asking why there's no simpler alternative built-in the language.

I think a better way would probably being able to do something in the def itself, in which the name argument would be attached to a "local", or "new" within the def, mutable object. Something like:

def better_append(new_item, a_list=immutable([])):
    a_list.append(new_item)
    return a_list

I'm sure someone can come with a better syntax, but I'm also guessing there must be a very good explanation to why this hasn't been done.

+9  A: 

This is called the 'mutable defaults trap'. See: http://www.ferg.org/projects/python_gotchas.html#contents_item_6

Basically, a_list is initialized when the program is first interpreted, not each time you call the function (as you might expect from other languages). So you're not getting a new list each time you call the function, but you're reusing the same one.

I guess the answer to the question is that if you want to append something to a list, just do it, don't create a function to do it.

This:

>>> my_list = []
>>> my_list.append(1)

Is clearer and easier to read than:

>>> my_list = my_append(1)

In the practical case, if you needed this sort of behavior, you would probably create your own class which has methods to manage it's internal list.

Seth
Sorry @Cawas, I am posting stream-of-consciousness style this morning. See edits.
Seth
Another thing to take away is that it is generally a good idea not to ever default parameters to empty mutable objects like [] or {}.
Justin Peel
@Seth, your edit came 7 seconds after my comment, so I deleted it ;) While I do want to figure my own way to write that to look better, the question here is **"why Python is like that"** and not *"how can I fix it"*. I'm just trying to understand better the concepts behind it.
Cawas
+1 because i really enjoyed learning about this
David McDavidson
+1  A: 

Probably you should not define these two functions as good and bad. You can use the first one with list or dictionaries to implement in place modifications of the corresponding objects. This method can give you headaches if you do not know how mutable objects work but given you known what you are doing it is OK in my opinion.

So you have two different methods to pass parameters providing different behaviors. And this is good, I would not change it.

joaquin
I did not gave the names, they come from python.net
Cawas
@Cawas, I know and I sympathize with your question. My answer was that *you* should not...
joaquin
+3  A: 

Default arguments are evaluated at the time the def statement is executed, which is the probably the most reasonable approach: it is often what is wanted. If it wasn't the case, it could cause confusing results when the environment changes a little.

Differentiating with a magic local method or something like that is far from ideal. Python tries to make things pretty plain and there is no obvious, clear replacement for the current boilerplate that doesn't resort to messing with the rather consistent semantics Python currently has.

Mike Graham
Now, this is more the kind of answer I was expecting. But why do you say Python semantics is so consistent? I tend to think we always have room for improvement. What about having a different statement from `def` that isn't evaluated at the time of execution, to make them be evaluated after any kind of mutable or immutable object?
Cawas
I didn't say there wasn't room for improvement; I think there are many things about Python that are suboptimal. I fully admit this is a nasty little thing you have to deal with. However, in the tough decision to be made here, I don't think I've seen a solution that works better without a significant change to how Python works.
Mike Graham
The other basic option is to, instead of passing an object as a default argument, to pass an expression that is evaluated each time the function is called. This would introduce a new thing to Python that defers execution (heretoforth functions are the only real thing that does it) and potentially change the algorithmic complexity of some operations from O(1) to O(n) (if an expensive operation is conducted time after time). It would require a new pattern `foo = bar(); def baz(qux=foo)` to do the opposite of `def(qux=sentinel): if qux is sentinel: qux = bar()` when it matters for your case.
Mike Graham
The fact that this pattern would probably come up less often is somewhat encouraging, but would have the negative effect of less awareness. (It's also far too late in the game to change this outright for a widely-used language like Python.)
Mike Graham
The compromise, adding syntax to say which case you want to eliminate needing a pattern at all is worth thinking about, but I don't think options like a "local" attribute-like thing come anywhere close to being worth it. A new keyword would have to be introduced, the language changed and made bigger, and the final solution doesn't seem that much neater to me. It is no more easily explained than the original pattern; perhaps the opposite. What's more, hitting this pattern usually helps someone to understand Python name and object semantics earlier on than anything else.
Mike Graham
As one final note, one sort of cool solution to this is to use functions, which I said do what you want—put off the evaluation of an expression. You could do `def baz(qux=lambda: bar(spam))` and then call qux within your function. This would require that people call your function like `baz(lambda: 3.5)`, which most people would think is weird in most circumstances. I'm not sure I'd say this is a good solution exactly (it's certainly not too popular in this exact form), but it is a way Python would let you use it if you chose to.
Mike Graham
The `.local` idea came from defining a new kind of mutable object that would behave on the function just like the immutable. Here's another idea: how about having a way to make any object into mutable and immutable? From the keywords semantics alone I'd say this should be possible, but it seems like it's pre-defined which object is what mutation-kind. To me that would make sense and could fix this issue quite well, and maybe destroy or help a lot of other things I can't even imagine as of now. :P what you say?
Cawas
+1  A: 

What if you were not talking about lists, but about AwesomeSets, a class you just defined? Would you want to define ".local" in every class?

class Foo(object):
    def get(self):
        return Foo()
    local = property(get)

could possibly work, but would get old really quick, really soon. Pretty soon, the "if a is None: a = CorrectObject()" pattern becomes second nature, and you won't find it ugly -- you'll find it illuminating.

The problem is not one of syntax, but one of semantics -- the values of default parameters are evaluated at function definition time, not at function execution time.

moshez
Sooo, what you're saying we eventually just get so used with the ugly syntax that we can't see it as ugly anymore? As I said there, ".local" might not be the better option, I don't know, but I think something should go *right there* in the argument definition, as this is a definition of the "function". Plus it would look much better.
Cawas
Note that this property does not do what you want because it is still evaluated exactly once. The magic `local` attribute would have to be syntax, not a property.
Mike Graham
+3  A: 

The extremely specific use case of a function that lets you optionally pass a list to modify, but generates a new list unless you specifically do pass one in, is definitely not worth a special-case syntax. Seriously, if you're making a number of calls to this function, why ever would you want to special-case the first call in the series (by passing only one argument) to distinguish it from every other one (which will need two arguments to be able to keep enriching an existing list)?! E.g., consider something like (assuming of course that betterappend did something useful, because in the current example it would be crazy to call it in lieu of a direct .append!-):

def thecaller(n):
  if fee(0):
    newlist = betterappend(foo())
  else:
    newlist = betterappend(fie())
  for x in range(1, n):
    if fee(x):
      betterappend(foo(), newlist)
    else:
      betterappend(fie(), newlist)

this is simply insane, and should obviously be, instead,

def thecaller(n):
  newlist = []
  for x in range(n):
    if fee(x):
      betterappend(foo(), newlist)
    else:
      betterappend(fie(), newlist)

always using two arguments, avoiding repetition, and building much simpler logic.

Introducing special-case syntax encourages and supports the special-cased use case, and there's really not much sense in encouraging and supporting this extremely peculiar one -- the existing, perfectly regular syntax is just fine for the use case's extremely rare good uses;-).

Alex Martelli
Alex, the first code don't need the if as well. Just write samething as second, but replace `betterappend(foo(), newlist)` to `newlist = betterappend(foo())`. But thanks for pointing this is potentially a "extremely specific" use case... Maybe it's just a matter of re-thinking on how to code, like the *"variable vs name"* little milestone.
Cawas
A: 

I think you're confusing elegant syntax with syntactic sugar. The python syntax communicates both approaches clearly, it just happens that the correct approach appears less elegant (in terms of lines of syntax) than the incorrect approach. But since the incorrect approach, is well...incorrect, it's elegance is irrelevant. As to why something like you demonstrate in better_append is not implemented, I would guess that There should be one-- and preferably only one --obvious way to do it. trumps minor gains in elegance.

cmsjr
Well, the `good_append` doesn't seem anything near obvious to me.
Cawas
I don't know. It's obvious to me that the good_append would work. It was not obvious to me that the bad_append wouldn't work. Perhaps a corollary of one obvious right way to do a given thing is that there are an arbitrary number of non-obvious wrong ways to do it.
cmsjr
@cmsjr precisely: it's not obvious why `bad_append` wouldn't work. That's what makes `good_append` not obvious to come up with, thus not an "obvious way to do it".
Cawas
That presupposes that bad_append is the more obvious approach, I'm not sure it is.Even if it is one of the less obvious features of language that strives for the obvious, it still seems more obvious than a specialized syntax to denote non-standard usage of parameters.
cmsjr
Given the syntax for immutable names, it is the more obvious approach for mutable ones. And without knowing is unlikely to use optional arguments, which is essentially what default argument does. I'm not suggesting a specialized syntax, I'm suggesting a generic syntax. Anyway, nobody here answered the real question yet: why it stays ugly like that? People are basically suggesting it's because "nobody came up with a better idea" from my point of view, since there's not a single quote from Python creators about it, just guesses of experienced users.
Cawas
A: 

This is better than good_append(), IMO:

def ok_append(new_item, a_list=None):
    return a_list.append(new_item) if a_list else [ new_item ]

You could also be extra careful and check that a_list was a list...

Kevin Little
This behaves strangely when called like `some_list = []; ok_append(4, some_list)` in that it does not append to my list but makes a new one. This is why I always check `if foo is not None` and not `if foo` when I really mean the former. This is also odd in that it always returns `None` or a single-item list since `list.append` mutates the list and returns `None`.
Mike Graham
Typechecking for `list` wouldn't be "extra careful", it would simply be awful practice in Python.
Mike Graham
+1  A: 

I've edited this answer to include thoughts from the many comments posted in the question.

The example you give is flawed. It modifies the list that you pass it as a side effect. If that's how you intended the function to work, it wouldn't make sense to have a default argument. Nor would it make sense to return the updated list. Without a default argument, the problem goes away.

If the intent was to return a new list, you need to make a copy of the input list. Python prefers that things be explicit, so it's up to you to make the copy.

def better_append(new_item, a_list=[]): 
    new_list = list(a_list)
    new_list.append(new_item) 
    return new_list 

For something a little different, you can make a generator that can take a list or a generator as input:

def generator_append(new_item, a_list=[]):
    for x in a_list:
        yield x
    yield new_item

I think you're under the misconception that Python treats mutable and immutable default arguments differently; that's simply not true. Rather, the immutability of the argument makes you change your code in a subtle way to do the right thing automatically. Take your example and make it apply to a string rather than a list:

def string_append(new_item, a_string=''):
    a_string = a_string + new_item
    return a_string

This code doesn't change the passed string - it can't, because strings are immutable. It creates a new string, and assigns a_string to that new string. The default argument can be used over and over again because it doesn't change, you made a copy of it at the start.

Mark Ransom
Those are 3 very good points here! 1. Is this better than using the `None` workaround? 2. I totally agree with not having a default argument if the intent is really to modify the passed list, but isn't that just one more reason why it's a flawed behavior? 3. I really know nothing about generators yet (off to research for a little while).
Cawas
This is a very nice explanation of generators: http://stackoverflow.com/questions/231767/can-somebody-explain-me-this-python-yield-statement/231855#231855
Cawas