Welcome, guest | Sign In | My Account | Store | Cart

If you have a function that calls another, passing through an argument, you likely want the default argument of your function to match that of the called function.

Deferred default arguments is what I call the technique of using a sentinel for the default argument of your function and having the called function translate the sentinel into its own default argument.

You'll find a more thorough treatment after the recipe.

Python, 49 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
"""deferred_default_arg module

Deferred is singleton to be used in place of None as the sentinel
for deferred default arguments.

"""

import inspect

class DeferredType: pass
Deferred = DeferredType()


def handle_deferred(f, name, fname=None):
    """Turn the singleton Deferred object into a default arg.

      f - the function to which you will pass the argument.
      name - the local name that will be passed as the argument.
      fname - the argument name of f.  If this is not passed, the name
            is used.

    If the argument of f() doesn't have a default value, raise a
    TypeError.  This is what would normally happen if you tried to call
    f() without passing that argument.  That's effectively what you are
    doing in that situation when passing Deferred.

    This function is a lot simpler if you use the PEP 362 function
    signature object.

    """

    if not fname:
        fname = name
    val = inspect.stack()[1][0].f_locals[name]
    if val is not Deferred:
        return val

    notkwonlycount = f.__code__.co_argcount - f.__code__.co_kwonlyargcount
    if fname in f.__code__.co_varnames[notkwonlycount:]:
        try:
            return f.__kwdefaults__[fname]
        except KeyError:
            raise TypeError("Can't convert Deferred for {}".format(name))
    default_names = f.__code__.co_varnames[
            notkwonlycount - len(f.__defaults__):notkwonlycount]
    try:
        return f.__defaults__[default_names.index(fname)]
    except ValueError:
        raise TypeError("Can't convert Deferred for {}".format(name))

Deferred Default Arguments

Say you have two functions:

def f(x=1):
    "do something with x"

def g(x=1):
    f(x)

As you can see, f is just passing x through to g. In this situation you will probably want the default argument in f to always be the same as the default argument in g. Currently to make that happen you have to do one of the following:

  • manually synchronize them, like above;
  • use introspection to dynamically synchronize them (modifying __defaults__);
  • use a sentinel value to indicate that the default should be used.

Here's an example:

def f(x=1):
    if x is None:
        x = f.__defaults__[0]

def g(x=None):
    f(x)

While helpful, there are downsides:

  • If you want to have the default actually be None, you will have to use some other value for the sentinel. However, this renders the function inconsistent with the use of None as sentinel in other functions.
  • A global lookup (to f.__defaults__[0]) has to happen for each call where the sentinel is passed.
  • More clutter in your function definitions.
  • g() must use the sentinel that f() is set up to handle.
  • You can't tell from g()'s definition what the default will be (though maybe you don't care).
  • Suppose you add another function call, h(), in g() that takes the same argument as f(). Now you have to make sure h() is set to handle the sentinel. Otherwise h() may be expecting g() to have a proper default argument.
  • If the global name "f" is no longer bound to the function, the defaults probably won't be right.

This recipe provides a simple solution that mitigates these downsides.

See recipe 577786 for a cleaner and more elegant solution for handling deferred default arguments.

Example

Using this recipe, this example demonstrates a simple way to handle deferred default arguments.

def f(x=5):
    return x

def g(x=Deferred):
    x = handle_deferred(f, "x")
    return f(x)

assert f() == 5
assert f(1) == 1

assert g() == 5
assert g(1) == 1