27

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.

Community
  • 1
  • 1
cregox
  • 17,674
  • 15
  • 85
  • 116
  • 3
    bad_append and good_append are neither good nor bad. They both have their uses. – unutbu Apr 14 '10 at 18:24
  • 3
    honestly, I've yet to see something like bad_append which wasn't bad. If it wasn't actively buggy, it was just syntax abuse (trying to get out of defining an object). *Sometimes* it's reasonable in inner functions -- but even there, very rarely. – moshez Apr 14 '10 at 18:33
  • You can use `a_list = a_list or []` instead of that if block. – Cat Plus Plus Apr 14 '10 at 18:34
  • @unutbu agreed, but not the point. – cregox Apr 14 '10 at 18:37
  • @PiotrLegnica look at @Seth's link to ferg.org - it says you shouldn't do that and give some reasons why. – cregox Apr 14 '10 at 18:38
  • 1
    See http://stackoverflow.com/questions/1132941/least-astonishment-in-python-the-mutable-default-argument – Mark Ransom Apr 14 '10 at 18:42
  • @Cawas: of course it's not direct equivalent — if you want to explicitly check for `None` instead of boolean eval (which rarely makes a difference), then use `a_list = [] if a_list is None else a_list`. – Cat Plus Plus Apr 14 '10 at 20:10
  • If I may say so, the question is not really about `append` but rather about default parameters that are instances of a mutable type. – Raphaël Saint-Pierre Apr 14 '10 at 20:49
  • @Raphael true. changing title now. – cregox Apr 14 '10 at 21:14
  • By the way, still hoping for an actual answer. So far, 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. All I can see are guesses of experienced users and explanations on *how* it works. – cregox Apr 14 '10 at 21:24
  • The answer is that there's a logical explanation for the way things work the way they do, and no easy fix. See my link. Anything that fixes it would be a huge kludge. – Mark Ransom Apr 14 '10 at 22:40
  • @Mark tho it may not be too evident, I've already seem the link and voted it up. And thank you for posting it! But I don't get your answer "things work the way they do". I want the logical explanation. :) – cregox Apr 14 '10 at 22:50
  • Sorry, I thought it was obvious from the accepted answer. A function is an object in Python, and the default argument is member data to that object. If it's mutable, then you can mutate it. To change the behavior, you'd have to create a whole new type of object that creates a copy of itself every time you try to access it - that's what your 'local' attribute would have to accomplish. I know your example is the canonical one for this problem, but I don't think it's realistic - see my answer. – Mark Ransom Apr 15 '10 at 02:56
  • lol @Mark . Again, I've already saw your answer, and even commented on it. Thanks for the clarification but I could already understand *why it happens*. What I can't understand is **why there's no syntax to the expected behavior**, which is the "good_append". I'll try to edit the question, maybe I can make it clearer. – cregox Apr 15 '10 at 15:33
  • 5
    Let me be blunt: your example is *flawed*. Either you want to modify the passed list, or you want to generate a new one, but your example tries to do both at the same time. The ugliness is a direct result of not acknowledging the flaw. As my answer states, you either want to remove the default argument, or you want to make a copy of the input list - and Python philosophy prefers that the copy be explicit. I'd welcome a different example that states the problem in a more realistic light. – Mark Ransom Apr 16 '10 at 04:02
  • @Mark Ransom, If by "member data" you mean "attribute", this isn't the case. (In general, I could not agree more with what you are saying.) – Mike Graham Apr 16 '10 at 18:17
  • 1
    @Cawas, There isn't a **the** expected behaviour; there's only *your* expected behaviour. I expect the actual behaviour, personally. It seems horrendously obvious to me (because I am very used to Python's semantics). When I was new to Python, like most people, my expectations probably would have varied from case to case. A lot of people find the non-delayed expression evaluation in the case of a function call or an arithmetic expression, even if they don't in all cases. There are cases where an expression should be evaluated once and cases where it should be evaluated every time, and people – Mike Graham Apr 16 '10 at 18:21
  • new to Python are apt to expect the one they want to be the default at the moment they want it and the opposite when they want the opposite. – Mike Graham Apr 16 '10 at 18:22
  • @Mike I partially agree. But my point here is that there are enough people expecting this behavior to justify having this changed, rather than leaving it "confusing" as it is **if** there is a way to do that without harming the Python philosophy. Maybe my question here could also be *where does doing that would harm it*. – cregox Apr 16 '10 at 18:48
  • @Mark the copy for imutable objects isn't explicit. I think that's what doesn't make much sense to me but, even if it did, the syntax would still look ugly at the exchange of what? Some people here must understand that, and maybe that makes sense to them, so I'm asking them (or maybe you guys) of how better_append could be written without harming all those concepts behind Python. – cregox Apr 16 '10 at 18:53
  • 1
    @Cawas, I admit it would be good if this were nicer, but cannot come up with a way that it would be nicer. I do not think you do so by this suggestion of a special syntax thing that looks like an attribute since this requires the same knowledge, just using new, not-instantly-clear syntax instead of the old pattern with no new syntax required. Things like `.local` don't help people understand any better. – Mike Graham Apr 16 '10 at 19:06
  • @Mike cool! if someone answers (to the title) "because nobody came up with a nice idea on how to do that look good just yet" with a good background (like some official blog, or something talking about evolution of Python syntax from the inventor, you know?) showing that if a well made idea come up it will be implemented, then that's just the kind of answer I'd accept. – cregox Apr 16 '10 at 19:21
  • Reflecting on the issue with a full understanding will reveal why it is unlikely there is a better solution, especially at this point in the game. – Mike Graham Apr 16 '10 at 19:36
  • @Mike anyway, I just changed the example a little bit... In hope it would actually also be an alternative fix possible to do. – cregox Apr 16 '10 at 20:42
  • @Cawas, Python already has an immutable list, it's called a tuple. But of course since it's immutable, it doesn't have an append method. What you're looking for is some special attribute for arguments that says make a copy of the argument automatically each time the function is called. This is why I mentioned Python prefers explicit - you are required to explicitly copy the argument yourself rather than having Python do it implicitly. – Mark Ransom Apr 16 '10 at 20:54
  • Relevant to the discussion http://en.wikipedia.org/wiki/Troll_%28Internet%29 – jfs Apr 19 '10 at 05:25
  • Here's another example: http://stackoverflow.com/questions/2667688/simple-python-oo-issue. A list is being used to initialize an object. Here though the problem would persist even if you didn't use the default argument. – Mark Ransom Apr 19 '10 at 13:34
  • Complementary question - [Good uses for mutable default arguments](http://stackoverflow.com/questions/9158294/good-uses-for-mutable-function-argument-default-values) – Jonathan Livni Feb 06 '12 at 20:54
  • The link that was put into the question isn't actually about "why does the code have this result?", but "why was this design decision made?". As far as I can tell, this question is *effectively* the same; "why do we have to write this ugly thing?" can only really be answered in the same terms (if at all; "ugly" is subjective. – Karl Knechtel Mar 27 '23 at 07:59

8 Answers8

11

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
  • 45,033
  • 10
  • 85
  • 120
  • Sorry @Cawas, I am posting stream-of-consciousness style this morning. See edits. – Seth Apr 14 '10 at 18:31
  • 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 Apr 14 '10 at 18:33
  • 1
    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. – cregox Apr 14 '10 at 18:33
6

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
  • 73,987
  • 14
  • 101
  • 130
  • 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? – cregox Apr 16 '10 at 16:08
  • 1
    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 Apr 16 '10 at 17:44
  • 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 Apr 16 '10 at 17:49
  • 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 Apr 16 '10 at 17:50
  • 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 Apr 16 '10 at 17:56
  • 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 Apr 16 '10 at 17:59
  • 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? – cregox Apr 16 '10 at 19:14
  • Sadly, I haven't coded in Python ever since, so now I'm rusty... But reading most answers again, I don't know why I haven't accepted this one before! – cregox Jan 10 '11 at 21:55
  • @cregox your can use a [decorator](https://gist.github.com/elazarg/99e6d1450c3c85aa74317c1b01767d86) for that – Elazar Jun 16 '16 at 00:54
  • @Elazar and I guess you probably meant to comment on the question! ;P – cregox Jun 16 '16 at 08:23
  • 2
    @cregox Please avoid swearing on Stack Overflow. – Kyll Jun 16 '16 at 08:23
  • @Kyll you just helped me yet a bit more, probably unwillingly like Elazar there. Read again, I'm not swearing as I haven't attacked anyone. I had written you a proper reply with more curse words being used properly just as before - only as self referential - but it was immediately wiped from the platform. No warnings, no nothing. They make it look like a bug. I was also linking to you this video which might better explain how words work https://www.youtube.com/watch?v=0-DasdwMvMI if it might interest you. – cregox Jun 16 '16 at 08:45
5

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
  • 854,459
  • 170
  • 1,222
  • 1,395
  • 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. – cregox Apr 14 '10 at 18:45
  • +1, you're right. People don't usually design a method that accepts an argument and returns that same argument - even with the intention of making that argument optional. Know exception to this rule: `memcpy`. – João Portela Jan 20 '11 at 16:17
3

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
  • 299,747
  • 42
  • 398
  • 622
  • 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). – cregox Apr 14 '10 at 22:51
  • This is a very nice explanation of generators: http://stackoverflow.com/questions/231767/can-somebody-explain-me-this-python-yield-statement/231855#231855 – cregox Apr 14 '10 at 23:01
  • 1
    Well stated. The concept of "immutable" isn't reified in the Python implementation; it's merely a *categorization* that we apply after the fact, based on the interfaces of types. The problems with a list as a default argument don't occur because the list is mutable; they occur because it is muta**ted**. Using an immutable type merely denies you the ability to err in that way. – Karl Knechtel Sep 19 '22 at 00:02
2

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
  • 36,202
  • 1
  • 22
  • 14
  • 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. – cregox Apr 14 '10 at 19:00
  • 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 Apr 16 '10 at 18:09
1

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
  • 82,968
  • 29
  • 138
  • 152
0

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
  • 56,771
  • 11
  • 70
  • 62
  • 2
    Well, the `good_append` doesn't seem anything near obvious to me. – cregox Apr 14 '10 at 18:58
  • 2
    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 Apr 14 '10 at 19:05
  • 1
    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". – cregox Apr 14 '10 at 19:28
  • 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 Apr 14 '10 at 20:25
  • 1
    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. – cregox Apr 14 '10 at 21:21
0

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
  • 12,436
  • 5
  • 39
  • 47
  • 1
    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 Apr 16 '10 at 18:08
  • 2
    Typechecking for `list` wouldn't be "extra careful", it would simply be awful practice in Python. – Mike Graham Apr 16 '10 at 18:08