Default parameters

Bengt Richter bokr at oz.net
Sat Dec 20 08:18:26 EST 2003


On Sat, 20 Dec 2003 03:55:22 GMT, Carl Banks <imbosol at aerojockey.invalid> wrote:

>Bengt Richter wrote:
>> 
>> 
>> On Sat, 20 Dec 2003 01:43:00 GMT, Carl Banks <imbosol at aerojockey.invalid> wrote:
>> 
>>>Paul Rubin wrote:
>>>> 
>>>> 
>>>> Carl Banks <imbosol at aerojockey.invalid> writes:
>>>>> Consider something like this:
>>>>> 
>>>>>     def func(param=((1,2),(3,4),(5,6),(7,8))):
>>>>>         whatever
>>>>> 
>>>>> Do you really want to be building a big-ass nested tuple every time
>>>>> the function is called?
>>>> 
>>>> Come on, the compiler can easily recognize that that list is constant.
>>>
>>>Yes, but that doesn't account for all expensive parameters.  What
>>>about this:
>>>
>>>    DEFAULT_LIST = ((1,2),(3,4),(5,6),(7,8))
>>>
>>>    def func(param=DEFAULT_LIST):
>>>        pass
>>>
>>>Or this:
>>>
>>>    import external_module
>>>
>>>    def func(param=external_modules.create_constant_object()):
>>>        pass
>>>
>>>Or how about this:
>>>
>>>    def func(param={'1': 'A', '2': 'B', '3': 'C', '4': 'D'}):
>>>        pass
>>>
>>>
>>>The compiler couldn't optimize any of the above cases.
>> 
>> For the DEFAULT_LIST (tuple?) and that particular dict literal, why not?
>
>
>Well, the value of DEFAULT_LIST is not known a compile time (unless, I
>suppose, this happens to be in the main module or command prompt).
>The literal is not a constant, so the compiler couldn't optimize this.

Well, according to the argument, we would be dealing with an optimizing compiler,
so presumably the compiler would see a name DEFAULT_LIST and simply compile a
call-time binding of param to whatever DEFAULT_LIST was bound to, and not bother
further. It could notice that the DEFAULT_LIST binding was still undisturbed, and
that it was to an immutable tuple with no mutable elements, which ISTM is effectively
a constant, but that analysis would be irrelevant, since the semantics would be
copying pre-existing binding (which is pretty optimized anyway).

The dict literal looks to me to be made up entirely of immutable keys and values, so
the value of that literal expression seems to me to be a constant. If you had call time
evaluation, you would be evaluating that expression each time, and the result would be
a fresh mutable dict with that constant initial value each time. ISTM that could be
optimized as param=private_dict_compile_time_created_from_literal.copy().
OTOH, if you used a pre-computed binding like DEFAULT_LIST, and wrote

       SHARED_DICT = {'1': 'A', '2': 'B', '3': 'C', '4': 'D'}
       def func(param=SHARED_DICT):
           pass

then at def-time the compiler would not see the literal, but rather a name bound to
a mutable dict instance. The call-time effect would be to bind param to whatever SHARED_DICT
happened to be bound to, just like for DEFAULT_LIST. But the semantics, given analysis that
showed no change to the SHARED_DICT _binding_ before the func call, would be to share a single
mutable dict instance. This is unlike the semantics of 

       def func(param={'1': 'A', '2': 'B', '3': 'C', '4': 'D'}):
           pass

which implies a fresh mutable dict instance bound to param, with the same initial value
(thus "constant" in a shallow sense at least, which in this case is fully constant).

>
>(Remember, the idea is that default parameters should be evaluated at
>call time, which would require the compiler to put the evaluations
>inside the function's pseudo-code.  The compiler could optimize default
>parameters by evaluating them at compile time: but you can only do
>that with constants, for obvious reasons.)
Yes, but note the difference between evaluating a name and a fixed-value literal expression,
as noted above.
>
>
>>>Well, I don't have any data, but my gut feeling is this would be
>>>somewhat more than "miniscule" performance hit.  Seeing how pervasive
>>>default arguments are, I'm guessing it would be a very significant
>>>slowdown if default arguments had to be evaluated every call.
>>>
>>>But since I have no numbers, I won't say anything more about it.
>>>
>> Don't know if I got this right, but
>> 
>> [18:32] /d/Python23/Lib>egrep -c 'def .*=' *py |cut -d: -f 2|sum
>> Total = 816
>> [18:32] /d/Python23/Lib>egrep -c 'def ' *py |cut -d: -f 2|sum
>> Total = 4454
>> 
>> would seem to suggest pervasive ~ 816/4453
>> or a little less than 20%
>
>Well, if you don't like the particular adjective I used, feel free to
>substitute another.  This happens a lot to me in c.l.p (see Martelli).
Sorry, I didn't mean make anything "happen to" you, especially if it was unpleasant ;-)
I just meant to pick up on "pervasive" and "numbers" and try to provide some anecdotal data.

>All I'm saying is, default arguments are common in Python code, and
>slowing them down is probably going to be a significant performance
>hit.
Probably in specific cases, but other cases could have no hit at all, given optimization.

>
>(You probably underestimated a little bit anyways: some functions
>don't get to the default arguments until the second line.)
Agreed.

>
>
>> Of course that says nothing about which are typically called in hot
>> loops ;-) But I think it's a bad idea as a default way of operating
>> anyway. You can always program call-time evaluations
>> explicitly. Maybe som syntactic sugar could be arranged, but I think
>> I would rather have some sugar for the opposite instead -- i.e.,
>> being able to code a block of preset locals evaluated and bound
>> locally like current parameter defaults, but not being part of the
>> call signature.
>
>Well, personally, I don't see much use for non-constant default
>arguments, as we have them now, wheras they would be useful if you
>could get a fresh copy.  And, frankly, the default arguments feel like
>they should be evaluated at call time.  Now that we have nested
>scopes, there's no need for them to simulate closures.  So, from a
>purely language perspective, I think they ought to be evaluated at
>call time.
I'd worry a bit about the meaning of names used in initialization expressions
if their values are to be looked up at call time. E.g., do you really want

   a = 2
   def foo(x=a): print 'x =', x
   ...
   ...
   a = 'eh?'
   foo()

to print 'eh?' By the time you are past a lot of ...'s, ISTM the code intent is not
so clear. But you can make dynamic access to the current a as a default explicit by

   class Defer(object):
      def __init__(self, lam): self.lam = lam

   def foo(x=Defer(lambda:a)):
       if isinstance(x, Defer): x=x.lam()
       print 'x =', x

The semantics are different. I'd prefer to have the best of both worlds and be able
to do both, as now, though I might not object to some nice syntactic sugar along the
lines suggested by OP Stian Søiland. E.g., short spelling for the above Defer effect:

   def foo(x:a): print 'x =', x

>
>The only thing is, I very much doubt I'd be willing to take the
>performance hit for it.
Moore+PyPy => less worry about that in future, I think.

Regards,
Bengt Richter




More information about the Python-list mailing list