[Python-ideas] deferred default arguments

Steven D'Aprano steve at pearwood.info
Thu Jul 14 03:23:17 CEST 2011


Eric Snow wrote:

> As you know, you can use default arguments when defining a function.
> Sometimes you want to pass an argument through from a function's
> arguments to another function that the original ("outer") calls.  I
> run into this during inheritance-related calls and when interfacing
> between two compatible APIs.  Here's an example:
> 
> class X:
>     def f(self, name="N/A"):
>         ...
> 
> class Y(X):
>     def f(self, name="N/A"):
>         super().f(name)


The problem is wider than inheritance of classes. I often have a series 
of functions that share identical signatures, including defaults:

def mean(data, missing=False): pass
def variance(data, missing=False):  pass
def pvariance(data, missing=False):  pass
def stdev(data, missing=False):  pass
def pstdev(data, missing=False):  pass

For API consistency, a change to one signature requires corresponding 
changes to the others.

I'm not *entirely* sure that this is a problem that needs solving, 
although it is a nuisance. (See my final paragraph, below.) What do 
other languages provide?

Wild idea: if Python had a "signature object", you could do something 
like this:

common = signature(data, missing=False)
def mean*common: pass

where poor old * gets yet another meaning, namely "use this signature as 
the function parameter list". (I'm not wedded to that syntax, it was 
just the first idea that came to mind.)


> * Manually keep the two sync'ed (like above), a DRY violation.
> * Use introspection at definition time (using X.f.__defaults__ and
> X.f.__kwdefaults__), which is fragile (if the X.f attributes are
> changed later or the function definition's signature changes).

This becomes messy if you only want to "inherit" the default value for 
one parameter rather than all.


> * Use a dummy value to indicate that the default should be supplied by
> X's f().  This requires some clutter in the function and possibly a
> less obvious default argument.

I believe that the usual name for this is a sentinel.


> A New Solution
> ---------------
> 
> Provide a builtin version of a Deferred singleton, like None or
> NotImplemented.  When resolving arguments for a call, if an argument
> is this Deferred object, replace it with the default argument for that
> parameter on the called function, if there is one.  If there isn't,
> act as though that argument was not passed at all.

I don't believe that should be a public object. If it's public, people 
will say "Yes, but what do you do if you want the default to actually be 
the Deferred singleton?" It's the None-as-sentinel problem all over 
again... sometimes you want None to stand in for no value, and sometimes 
you want it to be a first class value.

Better(?) to make it syntax, and overload * yet again:

def f(arg, another_arg, name=*): pass

This has the advantage(?) that inside the body of f, name doesn't get a 
spurious value Deferred. If the caller doesn't supply a value for name, 
and f tries to use it (except as below), then you get an unambiguous 
UnboundLocalError.

The exception is, calling another function with it as an argument is 
permitted. This call in the body of the function:

     result = g(1, 3, name, keyword="spam")

behaves something like this:

     try:
         name
     except NameError:
         result = g(1, 3, keyword="spam")
     else:
         result = g(1, 3, name, keyword="spam")

only handled by the compiler.

Seems awfully complicated just to solve a few DRY violations. I'm not 
sure it is worth the added complexity of implementation, and the added 
cognitive burden of learning about it.

That's not to say that DRY violations in function signatures aren't a 
real problem -- I've run into them myself. But I question how large a 
problem they are, in practice. Language support solving them seems to me 
to be a case of using a sledgehammer to crack a peanut. DRY is mostly a 
problem for implementation, not interface: implementation should remain 
free to change rapidly, and so repeating yourself makes that harder. But 
interface (e.g. your library API, including function signatures) should 
be static for long periods of time, so redundancy in function signatures 
is less important.



-- 
Steven




More information about the Python-ideas mailing list