Dynamic default arguments in python functions

Question:

I have a need for functions with default arguments that have to be set at function runtime (such as empty lists, values derived from other arguments or data taken from the database) and I am currently using the following pattern to deal with this:

def foo(bar, baz=None):
    baz = baz if baz else blar()
    # Stuff

Where blar() gives the proper default value of baz which might change during execution. However, the baz = baz if baz else ... line strikes me as inelegant. Does anyone else have a better method of avoiding the one-time binding of default function arguments? Small, cross-platform libraries installable by pip are acceptable replacements.

Asked By: Drakekin

||

Answers:

No, that’s pretty much it. Usually you test for is None so you can safely pass in falsey values like 0 or "" etc.

def foo(bar, baz=None):
    baz = baz if baz is not None else blar()

The old fashioned way is the two liner. Some people may prefer this

def foo(bar, baz=None):
    if baz is None:
        baz = blar()
Answered By: John La Rooy

A quick&dirty example implementation of something that might work:

class DynDefault(object):
    def __init__(self, callback):
        self.callback = callback
    def __call__(self):
        return self.callback()

def dyn_default(func):
    def wrapper(*args, **kw):
        args = [arg() for arg in args if isinstance(arg, DynDefault) else arg]
        for k, v in kw.items():
            if isinstance(v, DynDefault):
                kw[k] = v()
        return func(*args, **kw)
    return wrapper

@dyn_default
def foo(bar, baaz=DynDefault(blar)):
    # problem solved
Answered By: bruno desthuilliers

You can replace

baz = baz if baz else blar()

with

baz = baz or blar()

if you’re still happy with just testing for falsy values instead of None.

Answered By: Alex Hall

You could do something like this:

def getArg():
  try:
      return arg
  except NameError:
      return 0

def foo(x, y=getArg):
    y = y()
    print(y)

foo(1) # Prints 0 (Default)

arg = 7 # Set by argparse?

foo(2)           # Prints 7 (Dynamic global)
foo(3, lambda:9) # Prints 9 (Dynamic passed)
Answered By: Gillespie
pip install dynamic-default-args

It is somewhat similar to other hacks, just more elegant.
The idea is to have a container class for dynamic default arguments, and a decorator that uses introspection to acquire the decorated function’s signature, then generate a dedicated wrapper for it.
For example, with this function:

from dynamic_default_args import dynamic_default_args, named_default

@dynamic_default_args(format_doc=True)
def foo(a, b=named_default(name='b', value=5),
        /,
        c=named_default(name='c', value=object),
        *d,
        e=1e-3, f=named_default(name='f', value='will it work?'),
        **g):
    """ A function with dynamic default arguments.
    Args:
        a: Required Positional-only argument a.
        b: Positional-only argument b. Dynamically defaults to {b}.
        c: Positional-or-keyword argument c. Dynamically defaults to {c}.
        *d: Varargs.
        e: Keyword-only argument e. Defaults to 1e-3.
        f: Keyword-only argument f. Dynamically defaults to {f}
        **g: Varkeywords.
    """
    print(f'Called with: a={a}, b={b}, c={c}, d={d}, e={e}, f={f}, g={g}')

As you may know, Python has 5 kinds of arguments classified by their positions relative to the syntax’s /, *, and **:

def f(po0, ..., /, pok0, ..., *args, kw0, kw1, ..., **kwargs):
      ----------   --------    |     --------------    |
      |            |           |     |                 |
      |            Positional- |     |             Varkeywords
      |            or-keyword  |     Keyword-only
      Positional-only        Varargs  

We generate a string expression expr that contains the definition of a wrapping function, and call the original function with arguments depending on their type following the above rule. Its content should look something like this:

def wrapper(a, b=b_, c=c_, *d, e=e_, f=f_, **g):
    return func(a,
                b.value if isinstance(b, named_default) else b,
                c.value if isinstance(c, named_default) else c,
                *d,
                e=e,
                f=f.value if isinstance(f, named_default) else f,
                **g)

After that, compile the expr with a context dictionary containing the default arguments b_, c_, e_, f_ taken from the signature of foo, the function func=foo, and our defined class named_default.

exec_locals = {}
exec(compile(expr, '<foo_wrapper>', 'exec'), context, exec_locals)
wrapper = functools.wraps(func)(exec_locals[wrapper_alias])

All of these are executed at the beginning (not lazy initialized) so we can limit to one more function call at runtime, and a minimal amount of type checking and attribute accessing overheads (which is a lot more efficient than calling another function to retrieve the default value) for each function.

The container’s value can be modified later, and the function’s docstring will also be automatically reformatted.

named_default('b').value += 10
named_default('f').value = 'it works'
help(foo)
# foo(a, b=15, /, c=<class 'object'>, *d, e=0.001, f='it works!', **g)
#     A function with dynamic default arguments.
#     Args:
#         a: Required Positional-only argument a.
#         b: Positional-only argument b. Dynamically defaults to 6.
#         c: Positional-or-keyword argument c. Dynamically defaults to <class'object'>.
#         *d: Varargs.
#         e: Keyword-only argument e. Defaults to 1e-3.
#         f: Keyword-only argument f. Dynamically defaults to it works!
#         **g: Varkeywords.

Modifying foo.__defaults__ dynamically should also do the job and be more performant.

See more: dynamic-default-args

Answered By: Inspiros
Categories: questions Tags:
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.