0

I have a situation in which the value of the default argument in a function head is affected by an if-clause within the function body. I am using Python 3.7.3.

function definitions

We have two functions f and j. I understand the behavior of the first two function. I don't understand the second function's behavior.

def f(L=[]):
    print('f, inside:   ', L)
    L.append(5)
    return L

def j(L=[]):
    print('j, before if:', L)
    if L == []:
        L = []
    print('j, after if: ', L)
    L.append(5)
    return L

function behavior that I understand

Now we call the first function three times:

>>> print('f, return:   ', f())
f, inside:    []
f, return:    [5]
>>> print('f, return:   ', f())
f, inside:    [5]
f, return:    [5, 5]
>>> print('f, return:   ', f())
f, inside:    [5, 5]
f, return:    [5, 5, 5]

The empty list [] is initialized on the first call of the function. When we append 5 to L then the one instance of the list in the memory is modified. Hence, on the second function call, this modified list is assigned to L. Sounds reasonable.

function behavior that I don't unterstand

Now, we call the third function (j) and get:

>>> print('j, return:   ', j())
j, before if: []
j, after if:  []
j, return:    [5]
>>> print('j, return:   ', j())
j, before if: []
j, after if:  []
j, return:    [5]
>>> print('j, return:   ', j())
j, before if: []
j, after if:  []
j, return:    [5]

According to the output, L is an empty list in the beginning of each call of the function j. When I remove the if-clause, in the body of function j the function is equal to function f and yields the same output. Hence, the if-clause seems to have some side effect. Testing for len(L) == 0 instead of L == [] in j has the same effect.

related to

My question is related to:

But the answers to these question and the tutorial answer only, what I already know.

daniel.heydebreck
  • 768
  • 14
  • 22
  • 2
    This would be easier to follow if you would you post _one_ function whose behaviour you don't understand, explain your problem with it, and leave out all the other functions. – khelwood Jul 23 '19 at 13:21
  • 1
    It's unclear to me why that was unexpected/contrary. Were you expecting `L = []` inside the function to change the bound default value? – jonrsharpe Jul 23 '19 at 13:21
  • @khelwood I removed the fourth function. – daniel.heydebreck Jul 23 '19 at 13:24
  • @jonrsharpe I would expect that the function `j` has the same return value than function `f`. However, it does not. The print statements in the function body are for `debugging`. – daniel.heydebreck Jul 23 '19 at 13:27
  • 2
    But *why* did you expect that? What was your hypothesis for how that would work that led you to expect the same behaviour? – jonrsharpe Jul 23 '19 at 13:27
  • @daniel.neumann I meant leave out _all_ the other functions, except the one you are asking about. And explain clearly why the behaviour confuses you. – khelwood Jul 23 '19 at 13:29
  • @khelwood updated accordingly. – daniel.heydebreck Jul 23 '19 at 13:32
  • @jonrsharpe The `L` becomes differently initialized depending on the `if` clause in the function body. Please compare the lines `f, inside` and `j, before if` after the second call of `f` and `j`. In `f` the `L` is a list with value `5` in it. In `j` the `L` is an empty list. However, it should be a list `[5]`. – daniel.heydebreck Jul 23 '19 at 13:36
  • 1
    *Why* should it be? Again, *"Were you expecting `L = []` inside the function to change the bound default value?"* (or `L = [5]` the same) At the moment you seem to be asking *"what's the difference between appending multiple items to a single list and appending a single item each to multiple lists"*, and the answer seems obvious, so I'm trying to understand where your understanding departs what's happening. – jonrsharpe Jul 23 '19 at 13:39
  • @jonrsharpe PART 1: An empty list `[]` is initialized on the first call of the function `f`. The default for `L` is: point to this empty list. In the body of `f`, we append a value to the list and return it. On the second call of `f`, the empty list `[]` is not newly initialized but `L` points to the location in memory where the empty list was originally initialized. Since the first call of `f` this list is not empty anymore. Therefore, `L` does not have the value of an empty list but a list with one value. If we call `f` ten times, we will end up with a list of ten elements. – daniel.heydebreck Jul 23 '19 at 13:46
  • @daniel.neumann no, *per [the question](https://stackoverflow.com/questions/1132941/least-astonishment-and-the-mutable-default-argument) you've linked to* the empty list is initialised when the function is *created*, not when it's first called. – jonrsharpe Jul 23 '19 at 13:49
  • @jonrsharpe PART 2: Now we look into `j`. I expected `j` to work the same way. At least the print statement before the `if`-clause should yield `[5]` in the second call of `j`. – daniel.heydebreck Jul 23 '19 at 13:54
  • @jonrsharpe While I am typing these comments, the question was answered by glibdud . The list (towards which `L` points to) that is modified in `j` is not the list set as default value. – daniel.heydebreck Jul 23 '19 at 13:56
  • 1
    It feels like you could just have answered "yes" either time I asked and saved quite a bit of typing. The same answer was provided a while ago by Jean-François too – jonrsharpe Jul 23 '19 at 13:56

3 Answers3

2

Modify your print statements to also print id(L):

def j(L=[]):
    print('j, before if:', L, id(L))
    if L == []:
        L = []
    print('j, after if: ', L, id(L))
    L.append(5)
    return L

Now check your results:

>>> j()
j, before if: [] 2844163925576
j, after if:  [] 2844163967688
[5]

Note the difference in the IDs. By the time you get to the portion of the function where you modify L, it no longer refers to the same object as the default argument. You've rebound L to a new list (with the L = [] in the body of the if statement), and as a result, you are never changing the default argument.

glibdud
  • 7,550
  • 4
  • 27
  • 37
1

if all boils down to this:

if L == [5]:
    L = []

Here you're not changing the value of the default argument, just binding locally to a new name.

You can do that instead:

if L == [5]:
    L.clear()

or

if L == [5]:
    L[:] = []

to clear the data of the parameter object.

Jean-François Fabre
  • 137,073
  • 23
  • 153
  • 219
1
def a():
    print "assigning default value"
    return []

def b(x=a()):
    x.append(10)
    print "inside b",x
    return x

print b()
print b()
print b()

try running this. You'll see that default value is not assigned every time you run the function OUTPUT

assigning default value
inside b [10]
[10]
inside b [10, 10]
[10, 10]
inside b [10, 10, 10]
[10, 10, 10]

only once it called the 'a' function to the default value. rest is very well explained above about the compilation of a method so not repeating the same