Reputation: 1348
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.
Upvotes: 8
Views: 6879
Reputation: 11
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
Upvotes: 1
Reputation: 6561
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)
Upvotes: 0
Reputation: 36043
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
.
Upvotes: 7
Reputation: 304473
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()
Upvotes: 12
Reputation: 77942
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
Upvotes: 0