Why are default arguments evaluated at definition time? Why are default arguments evaluated at definition time? python python

Why are default arguments evaluated at definition time?


The alternative would be quite heavyweight -- storing "default argument values" in the function object as "thunks" of code to be executed over and over again every time the function is called without a specified value for that argument -- and would make it much harder to get early binding (binding at def time), which is often what you want. For example, in Python as it exists:

def ack(m, n, _memo={}):  key = m, n  if key not in _memo:    if m==0: v = n + 1    elif n==0: v = ack(m-1, 1)    else: v = ack(m-1, ack(m, n-1))    _memo[key] = v  return _memo[key]

...writing a memoized function like the above is quite an elementary task. Similarly:

for i in range(len(buttons)):  buttons[i].onclick(lambda i=i: say('button %s', i))

...the simple i=i, relying on the early-binding (definition time) of default arg values, is a trivially simple way to get early binding. So, the current rule is simple, straightforward, and lets you do all you want in a way that's extremely easy to explain and understand: if you want late binding of an expression's value, evaluate that expression in the function body; if you want early binding, evaluate it as the default value of an arg.

The alternative, forcing late binding for both situation, would not offer this flexibility, and would force you to go through hoops (such as wrapping your function into a closure factory) every time you needed early binding, as in the above examples -- yet more heavy-weight boilerplate forced on the programmer by this hypothetical design decision (beyond the "invisible" ones of generating and repeatedly evaluating thunks all over the place).

In other words, "There should be one, and preferably only one, obvious way to do it [1]": when you want late binding, there's already a perfectly obvious way to achieve it (since all of the function's code is only executed at call time, obviously everything evaluated there is late-bound); having default-arg evaluation produce early binding gives you an obvious way to achieve early binding as well (a plus!-) rather than giving TWO obvious ways to get late binding and no obvious way to get early binding (a minus!-).

[1]: "Although that way may not be obvious at first unless you're Dutch."


The issue is this.

It's too expensive to evaluate a function as an initializer every time the function is called.

  • 0 is a simple literal. Evaluate it once, use it forever.

  • int is a function (like list) that would have to be evaluated each time it's required as an initializer.

The construct [] is literal, like 0, that means "this exact object".

The problem is that some people hope that it to means list as in "evaluate this function for me, please, to get the object that is the initializer".

It would be a crushing burden to add the necessary if statement to do this evaluation all the time. It's better to take all arguments as literals and not do any additional function evaluation as part of trying to do a function evaluation.

Also, more fundamentally, it's technically impossible to implement argument defaults as function evaluations.

Consider, for a moment the recursive horror of this kind of circularity. Let's say that instead of default values being literals, we allow them to be functions which are evaluated each time a parameter's default values are required.

[This would parallel the way collections.defaultdict works.]

def aFunc( a=another_func ):    return a*2def another_func( b=aFunc ):    return b*3

What is the value of another_func()? To get the default for b, it must evaluate aFunc, which requires an eval of another_func. Oops.


Of course in your situation it is difficult to understand. But you must see, that evaluating default args every time would lay a heavy runtime burden on the system.

Also you should know, that in case of container types this problem may occur -- but you could circumvent it by making the thing explicit:

def __init__(self, children = None):    if children is None:       children = []    self.children = children