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