[Python-ideas] proto-PEP: Fixing Non-constant Default Arguments

Chris Rebert cvrebert at gmail.com
Sun Jan 28 20:22:44 CET 2007


The following is a proto-PEP based on the discussion in the thread
"fixing mutable default argument values". Comments would be greatly
appreciated.
- Chris Rebert

Title: Fixing Non-constant Default Arguments

Abstract

     This PEP proposes new semantics for default arguments to remove 
boilerplate code associated with non-constant default argument values,
allowing them to be expressed more clearly and succinctly.


Motivation

     Currently, to write functions using non-constant default arguments, 
one must use the idiom:

     def foo(non_const=None):
         if non_const is None:
             non_const = some_expr
         #rest of function

or equivalent code. Naive programmers desiring mutable default arguments 
often make the mistake of writing the following:

     def foo(mutable=some_expr_producing_mutable):
         #rest of function

However, this does not work as intended, as 
'some_expr_producing_mutable' is evaluated only *once* at 
definition-time, rather than once per call at call-time.  This results 
in all calls to 'foo' using the same default value, which can result in 
unintended consequences.  This necessitates the previously mentioned 
idiom. This unintuitive behavior is such a frequent stumbling block for 
newbies that it is present in at least 3 lists of Python's problems [0] 
[1] [2].
     There are currently few, if any, known good uses of the current 
behavior of mutable default arguments.  The most common one is to 
preserve function state between calls.  However, as one of the lists [2] 
comments, this purpose is much better served by decorators, classes, or 
(though less preferred) global variables.
     Therefore, since the current semantics aren't useful for 
non-constant default values and an idiom is necessary to work around 
this deficiency, why not change the semantics so that people can write 
what they mean more directly, without the annoying boilerplate?


Rationale

     Originally, it was proposed that all default argument values be 
deep-copied from the original (evaluated at definition-time) at each 
invocation of the function where the default value was required. 
However, this doesn't take into account default values that are not 
literals, e.g. function calls, subscripts, attribute accesses.  Thus, 
the new idea was to re-evaluate the default arguments at each call where 
they were needed. There was some concern over the possible performance 
hit this could cause, and whether there should be new syntax so that 
code could use the existing semantics for performance reasons.  Some of 
the proposed syntaxes were:

     def foo(bar=<baz>):
         #code

     def foo(bar=new baz):
         #code

     def foo(bar=fresh baz):
         #code

     def foo(bar=separate baz):
         #code

     def foo(bar=another baz):
         #code

     def foo(bar=unique baz):
         #code

where the new keyword (or angle brackets) would indicate that the 
parameter's default argument should use the new semantics.  Other 
parameters would continue to use the old semantics. It was generally 
agreed that the angle-bracket syntax was particularly ugly, leading to 
the proposal of the other syntaxes. However, having 2 different sets of 
semantics could be confusing and leaving in the old semantics just for 
performance might be premature optimization. Refactorings to deal with 
the possible performance hit are discussed below.


Specification

     The current semantics for default arguments are replaced by the 
following semantics:
     - Whenever a function is called, and the caller does not provide a
     value for a parameter with a default expression, the parameter's
     default expression shall be evaluated in the function's scope.  The
     resulting value shall be assigned to a local variable in the
     function's scope with the same name as the parameter.
     - The default argument expressions shall be evaluated before the
     body of the function.
     - The evaluation of default argument expressions shall proceed in
     the same order as that of the parameter list in the function's
     definition.
Given these semantics, it makes more sense to refer to default argument
expressions rather than default argument values, as the expression is
re-evaluated at each call, rather than just once at definition-time.
Therefore, we shall do so hereafter.

Demonstrative examples of new semantics:
     #default argument expressions can refer to
     #variables in the enclosing scope...
     CONST = "hi"
     def foo(a=CONST):
         print a

     >>> foo()
     hi
     >>> CONST="bye"
     >>> foo()
     bye

     #...or even other arguments
     def ncopies(container, n=len(container)):
         return [container for i in range(n)]

     >>> ncopies([1, 2], 5)
     [[1, 2], [1, 2], [1, 2], [1, 2], [1, 2]]
     >>> ncopies([1, 2, 3])
     [[1, 2, 3], [1, 2, 3], [1, 2, 3]]
     >>> #ncopies grabbed n from [1, 2, 3]'s length (3)

     #default argument expressions are arbitrary expressions
     def my_sum(lst):
         cur_sum = lst[0]
         for i in lst[1:]: cur_sum += i
         return cur_sum

     def bar(b=my_sum((["b"] * (2 * 3))[:4])):
         print b

     >>> bar()
     bbbb

     #default argument expressions are re-evaluated at every call...
     from random import randint
     def baz(c=randint(1,3)):
         print c

     >>> baz()
     2
     >>> baz()
     3

     #...but only when they're required
     def silly():
         print "spam"
         return 42

     def qux(d=silly()):
         pass

     >>> qux()
     spam
     >>> qux(17)
     >>> qux(d=17)
     >>> qux(*[17])
     >>> qux(**{'d':17})
     >>> #no output because silly() never called because d's value was 
specified in the calls

     #Rule 3
     count = 0
     def next():
         global count
         count += 1
         return count - 1

     def frobnicate(g=next(), h=next(), i=next()):
         print g, h, i

     >>> frobnicate()
     0 1 2
     >>> #g, h, and i's default argument expressions are evaluated in 
the same order as the parameter definition


Backwards Compatibility

     This change in semantics breaks all code which uses mutable default 
argument values. Such code can be refactored from:

     def foo(bar=mutable):
         #code

to

     def stateify(state):
         def _wrap(func):
             def _wrapper(*args, **kwds):
                 kwds['bar'] = state
                 return func(*args, **kwds)
             return _wrapper
         return _wrap

     @stateify(mutable)
     def foo(bar):
         #code

or

     state = mutable
     def foo(bar=state):
         #code

or

     class Baz(object):
         def __init__(self):
             self.state = mutable

         def foo(self, bar=self.state):
             #code

The changes in this PEP are backwards-compatible with all code whose 
default argument values are immutable, including code using the idiom 
mentioned in the 'Motivation' section. However, such values will now be 
recomputed for each call for which they are required. This may cause 
performance degradation. If such recomputation is significantly 
expensive, the same refactorings mentioned above can be used.

     In relation to Python 3.0, this PEP's proposal is compatible with 
those of PEP 3102 [3] and PEP 3107 [4]. Also, this PEP does not depend 
on the acceptance of either of those PEPs.


Reference Implementation

     All code of the form:

         def foo(bar=some_expr, baz=other_expr):
             #body

     Should act as if it had read (in pseudo-Python):

         def foo(bar=_undefined, baz=_undefined):
             if bar is _undefined:
                 bar = some_expr
             if baz is _undefined:
                 baz = other_expr
             #body

where _undefined is the value given to a parameter when the caller 
didn't specify a value for it. This is not intended to be a literal 
translation, but rather a demonstration as to how Python's internal 
argument-handling machinery should be changed.


References

     [0] 10 Python pitfalls
         http://zephyrfalcon.org/labs/python_pitfalls.html

     [1] Python Gotchas
         http://www.ferg.org/projects/python_gotchas.html#contents_item_6

     [2] When Pythons Attack
     http://www.onlamp.com/pub/a/python/2004/02/05/learn_python.html?page=2

     [3] Keyword-Only Arguments
         http://www.python.org/dev/peps/pep-3102/

     [4] Function Annotations
         http://www.python.org/dev/peps/pep-3107/



More information about the Python-ideas mailing list