2

I stumbled upon a strange behavior I like to get explained. To illustrate, consider the following function (written inside a Jupyter cell):

def testfunc(res = [], a = {}, i = 0):
    if i < 3:
        b = dict(a)               # make a local copy to keep in this recursion level
        b[str(chr(97+i))] = i     # fill some values
        res.append(b)             # push into result
        res = testfunc(res,b,i+1) # recursion

    return res

print(testfunc())

Now the output is, as expected:

[{'a': 0}, {'a': 0, 'b': 1}, {'a': 0, 'b': 1, 'c': 2}]

and if I output 'res', it is as expected not in scope anymore:

print(res)

NameError: name 'res' is not defined

So, far so good. Now however, if I call the function again in a new cell:

print(testfunc())

this happens:

[{'a': 0}, {'a': 0, 'b': 1}, {'a': 0, 'b': 1, 'c': 2}, {'a': 0}, {'a': 0, 'b': 1}, {'a': 0, 'b': 1, 'c': 2}]

Which leads to the assumption, that res was not destroyed at the end of the function and was not reassigned with an empty array as the default parameter exacts. Instead, the parameter with the content of the former call is reused which I consider a very odd and unexpected behavior. Strangely, this is only happening with the object (list), not the integer, probably because it's a primitive data type.

Is this supposed to be? Why is the explicit assignment of the default parameter ignored, when the function is called with no parameter and instead the variable of a former function call is used? What is the benefit of such a behavior?

I understand that I can work around this issue by forwarding an empty list like this

testfunc([])

and not using the default parameter, which makes default parameters basically not only useless but dangerous, as their usage seems to be unpredictable. Or do I miss something?

phx16
  • 37
  • 5

1 Answers1

3

This is because the res variable points to the same list object. when you executed it the first time, the list was changed and new elements were added in it. In the second iteration of the code, the same list was appended. Consider this example below:

def testfunc(l1 = []):
    l1.append(1)
    return l1

if you execute below code:

testfunc()
testfunc()
testfunc()

will return :

1
1,1
1,1,1

This is as stated in python docs at https://docs.python.org/3/tutorial/controlflow.html#more-on-defining-functions

Important warning: The default value is evaluated only once. This makes a difference when the default is a mutable object such as a list, dictionary, or instances of most classes. For example, the following function accumulates the arguments passed to it on subsequent calls:

Vijay Jangir
  • 584
  • 3
  • 15
  • I had searched for this behavior but not knowing the source of the cause makes it sometimes difficult to use the right phrase to get the proper result. I do however agree with the popular opinion that this is a design flaw, even thou it might be exploited to good use. – phx16 Apr 16 '19 at 14:21