Why does my function remember a default argument?

The following behaviour happened when I was trying to memoize a function and I can’t quite wrap my head around it. For the sake of presenting a MWE here is a function that computes (integer, non-negative) powers of 2 in an unnecessarily recursive way:

def power2(n):
    if n == 0: return 1
    return 2 * power2(n-1)

And here is my attempt to memoize it with a dictionary:

def power2(n, memo={}):
    if n in memo: return memo[n]
    if n == 0: return 1
    memo[n] = 2 * power2(n-1, memo)
    return memo[n]

This works like a charm, but for the sake of debugging I’d also like the function to print what it’s doing upon calling. So here is a very talkative function that does the same:

def power2(n, memo={}):
    print("Computing 2 **", n)
    if n in memo:
        print("  I have 2 **", n, "in my memo! Returning.")
        return memo[n]
    print("  I don't have 2 **", n, "in my memo. Computing...")
    if n == 0:
        print("  The base case for induction has been reached. Returning.")
        return 1
    p = 2 * power2(n-1, memo)
    print("2 **", n, "=", p)
    memo[n] = p
    return memo[n]

Now if I call

power2(6)

the console prints

Computing 2 ** 4
  I don't have 2 ** 4 in my memo. Computing...
Computing 2 ** 3
  I don't have 2 ** 3 in my memo. Computing...
Computing 2 ** 2
  I don't have 2 ** 2 in my memo. Computing...
Computing 2 ** 1
  I don't have 2 ** 1 in my memo. Computing...
Computing 2 ** 0
  I don't have 2 ** 0 in my memo. Computing...
  The base case for induction has been reached.
Returning 2 ** 0 = 1
Returning 2 ** 1 = 2
Returning 2 ** 2 = 4
Returning 2 ** 3 = 8
Returning 2 ** 4 = 16

Which is what I expected: the memo here serves absolutely no purpose because we always pass a different argument to the function everytime we call it.

What I don’t understand is that, if I call the function twice on the same argument, eg:

power2(4)
power2(4)

what I get is the following:

Computing 2 ** 4
  I don't have 2 ** 4 in my memo. Computing...
Computing 2 ** 3
  I don't have 2 ** 3 in my memo. Computing...
Computing 2 ** 2
  I don't have 2 ** 2 in my memo. Computing...
Computing 2 ** 1
  I don't have 2 ** 1 in my memo. Computing...
Computing 2 ** 0
  I don't have 2 ** 0 in my memo. Computing...
  The base case for induction has been reached.
Returning 2 ** 0 = 1
Returning 2 ** 1 = 2
Returning 2 ** 2 = 4
Returning 2 ** 3 = 8
Returning 2 ** 4 = 16
Computing 2 ** 4
  I have 2 ** 4 in my memo! Returning.

Similarly if the second time I call the function I pass, as an argument, a lower number (it just fetches the value from memo) or a higher number (it performs recursion only until it finds the corresponding key in memo).

Questions:

  1. Why does the function power2 remember the content of memo? If I call it without passing the argument memo, why doesn’t it default at {}?
  2. If power2 remembers memo, does it mean that I can also access its content after calling the function the first time? If so, how?
  3. Suppose the intended behaviour is for the function to actually forget the dictionary unless I pass one as second argument. Is there a smart/standard way to implement it, or should I just keep calling functions explicitly passing memo={}? (In this case it does not seem very useful to even assign a default empty dictionary to memo in the function definition!)

Thanks for any help

  1. Default values of the parameters are evaluated just once, on the function definition. This means if used default value has mutable data type, it is possible for mutated parameter from one function call, impact the other function calls.
  2. Inside of function - yes. It’s memo when second parameter is not passed when calling function.
  3. The typical pattern for that in python is to use None as default value, and then in function for example:
if memo is None:
    memo = {}
1 Like

I see. Is there a… “deep” reason why the evaluation is designed this way?

ooh, clever

Thank you for your answer!

I cannot say much about specific reasons, what they were exactly, how much this was on purpose or if it’s just a result of other design decisions. It seems though that deliberately there are no specific plans for changing this.

If you search for python mutable default value in web search, there’s many discussions about this, going into details and python internals.

1 Like

Got it. (I asked in case there was a really obvious way to think about it)

Thanks again!