[Python-ideas] deferred default arguments

Eric Snow ericsnowcurrently at gmail.com
Wed Jul 13 21:26:17 CEST 2011


I'll get this out of the way now.  This is probably a dumb idea.
However, I have found myself working around a use case lately and have
a thought of a cleaner solution.


The Situation
---------------

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)

Y().f()

So, the name argument is passed through from Y's f() to X's.  In this
case, we want to keep the default argument the same.


Current Solutions
---------------

Keeping the default arguments the same can be accomplished in a few
different ways right now, but they are less than ideal:

* 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).
* 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.

Here's an example of that last one:

class DeferredType: pass
DEFERRED = DeferredType()

class X1:
    def f(self, name="N/A"):
        if name is DEFERRED:
            name = "N/A" # DRY
            #name = X1.f.__defaults__[0] # fragile
        ...

class Y1(X1):
    def f(self, name=DEFERRED):
        super().f(name)

Of course, the DEFERRED object could be any other specific object,
particularly None.  Here's another similar example that reminds me of
Jack's example[1] from the recent thread on mutable default arguments.
 It is the solution I am using currently.

class X2:
    def f(self, name=None):
        if name is None:
            name = "N/A"
        ...

class Y2(X2):
    def f(self, name=None):
        super().f(name)

Of course, using DEFERRED instead of None would allow you to use None
as the actual default argument, but you get the idea.  Notice that the
actual default argument is no longer in the argument list, so it is
less obvious and no longer introspect-able.

I have also run into this use-case outside of inheritance situations.


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.

Here's how it would look:

class X:
    def f(self, name="N/A"):
        ...

class Y(X):
    def f(self, name=Deferred):
        super().f(name)

[2]

This way, Y's f has a sensible default argument for "name"[3] and
implies where to find the actual default argument (on super().f)[4].
X's f is not cluttered up and has a clear, introspect-able default
argument.

Thoughts?

-eric


p.s. while I usually pursue random flights of fancy (answers looking
for questions), this idea was born of an honest-to-goodness practical
use-case I use semi-frequently.  One thing I've learned about the
Python community is that ideas that come from real [and common]
use-cases have a much better chance of not dying a quick and painful
death (not holding my breath here though <wink>).

[1] http://mail.python.org/pipermail/python-ideas/2011-May/010263.html
[2] As a bonus, an "outer" function could take advantage of different
default arguments when calling more than one function:

def f(name="N/A"):
    ...
def g(name="Undefined"):
    ...
def h(name=Deferred):
    f(name)
    g(name)

[3] "Deferred" indicates what kind of argument it is in a
straightforward way.  However, I don't want the name to be confused
with anything like Twisted Deferred objects, so whatever name is clear
would, of course, be appropriate.
[4] That is true from a people perspective, but not for introspection;
e.g. if you want to introspect the actual default argument for Y.f,
you would have to be able to programmatically determine how that
parameter is used in the function body...



More information about the Python-ideas mailing list