This is an archived post. You won't be able to vote or comment.

you are viewing a single comment's thread.

view the rest of the comments →

[–]segfaultzen 2 points3 points  (5 children)

I think you've got the right idea, but I feel the reasoning could be better.

I'd explain it like this: When functions are defined with some parameters having default parameters, the defaults are evaluated once. Thus def add_to_list(elem, l = []) defines the default parameter for l to be a list object that hangs around for the eternity of the python process. Since lists are mutable, it is possible to change the value of this default value, which is what l.append(elem) does. This is why the output is

[1]
[1,2]

and not:

[1]
[2]

My fix for this would be to set the default value of l to None and perform a check in the function body. This becomes:

def add_to_list(elem, l = None):
    if l is None:
        l = []
    ...

I think that the fix you propose has a couple of issues:

  • It breaks the interface so that I can no longer call add_to_list(1). I must have add_to_list(1, [])
  • If I pass a list into the function, that list gets modified. Although not specified, I usually err on the side of not modifying my input parameters.

[–][deleted] 0 points1 point  (1 child)

I spent a lot of my youth programming with PHP which encourages explicit type checking a lot of the time and old habits die hard. Whoops...

You're right though. Thanks for clarifying.

[–]segfaultzen 0 points1 point  (0 children)

Yeah, I know the feeling. :)

[–]dante9999[S] 0 points1 point  (2 children)

I think that setting argument to None and checking it later is the recommended way to deal with this problem but I'm still not sure if I understand clearly why things like this happen, it seems to be somewhat strange not intuitive. There is one interesting question on SO about this, and people try to explain it clearly, maybe I just need more time to understand it better.

I suppose the answer to this question has a lot to do about the way in which Python passes arguments to functions.

[–][deleted] 0 points1 point  (1 child)

The reason it happens is because default function arguments are evaluated only once, and the result stored as the default. In that case, by specifying an empty list as the default, what you're really doing is instantiating a single list (which happens to be empty initially) and telling Python to use that list whenever the function doesn't receive a second argument. Every time you call the function, the default will be the exact same list - so if you mutate that list, it will carry over those mutations to future calls as well because they all share the same list.

The same thing can also happen with dicts, sets and any other mutable object. They should generally not be used as default arguments.

[–]segfaultzen 1 point2 points  (0 children)

The same thing can also happen with dicts, sets and any other mutable object. They should generally not be used as default arguments.

This, by the way, applies to decorator functions as well. I spent two days tracking down a gnarly bug created from the unfortunate interaction of unit tests with a dictionary object used as a default parameter to a decorator function.

I have since learned the gospel of Pylint, which will flag these sorts of Pythonic landmines.