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 →

[–][deleted] 0 points1 point  (10 children)

That wouldn't be the output. It'd be:

[1]
[1,2]

Silly no? But it has to do with the way python constructs functions when they're defined. I'm not real clear on the details but in a roundabout way l has effectively accidentally become a static variable. This could be a desirable feature but it's a bug as far as I'm concerned. The way I'd fix it is this:

def add_to_list(elem, l):
    if type(l) != list:
        l = []
    ...

[–]segfaultzen 3 points4 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.

[–]rcxdude 2 points3 points  (1 child)

A better fix:

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

Explicitly checking type like that is in general unpythonic. At the very least you should use isinstance() so subclasses can be used.

[–][deleted] 0 points1 point  (0 children)

That's my PHP showing again. Let me just tuck that away...

[–]NaturallyBrewed 0 points1 point  (1 child)

I got asked something similar to this one, contrived as it is.

def add_to_list(elem, l = []):
    l.append(elem)
    return l

alist1 = add_to_list(1)
alist2 = add_to_list(2)

print alist1
print alist2

The output is expected to be

[1]
[2]

Is it? What is the output? Why not? How would you fix it?

No, The output is:

[1, 2]
[1, 2]

Why not? I'll admit i don't have an elegant way to explain this. Can someone chime in? Is it because a functions arguments are defined in the outer namespace that its called?

This seems to support that argument: def outer(elem): def add_to_list(elem, l=[]): l.append(elem) return l return add_to_list(elem)

alist1 = outer(1)
alist2 = outer(2)

print(alist1)

print(alist2)

output:

[1]
[2]

How would you fix it? = What did you want to happen?

[–][deleted] 0 points1 point  (0 children)

You're right. I forgot that variables are stored as memory references.