Avoiding code repetition in default arguments in Python
Define global constants:
ACCURACY = 1e-3NSTEP = 10def f(accuracy=ACCURACY, nstep=NSTEP): ...def g(accuracy=ACCURACY, nstep=NSTEP): f(accuracy, nstep)
If f
and g
are defined in different modules, then you could make a constants.py
module too:
ACCURACY = 1e-3NSTEP = 10
and then define f
with:
from constants import ACCURACY, NSTEPdef f(accuracy=ACCURACY, nstep=NSTEP): ...
and similarly for g
.
I think that procedural paradigm narrows your vision to that problem. Here are some solutions I found using other Python features.
Object-oriented programming
You're calling f()
and g()
with same subset of parameters -- this is good hint that these parameters represent same entity. Why not to make it an object?
class FG: def __init__(self, accuracy=1e-3, nstep=10): self.accuracy = accuracy self.nstep = nstep def f(self): print ('f', self.accuracy, self.nstep) def g(self): self.f() print ('g', self.accuracy, self.nstep)FG().f()FG(1e-5).g()FG(nstep=20).g()
Functional programming
You may convert f()
into higher-order function -- i.e. something like this:
from functools import partialdef g(accuracy, nstep): print ('g', accuracy, nstep)def f(accuracy=1e-3, nstep=10): g(accuracy, nstep) print ('f', accuracy, nstep)def fg(func, accuracy=1e-3, nstep=10): return partial(func, accuracy=accuracy, nstep=nstep)fg(g)()fg(f, 2e-5)()fg(f, nstep=32)()
But this is also a tricky approach -- f()
and g()
calls were swapped here. Probably there are better approaches to do that -- i.e. pipelines with callbacks, I'm not that good with FP :(
Dynamicness & introspection
This is much more complex approach, and it requires digging into CPython internals, but since CPython allows that, why not use it?
Here is a decorator to update default values through __defaults__
member:
class use_defaults: def __init__(self, deflt_func): self.deflt_func = deflt_func def __call__(self, func): defltargs = dict(zip(getargspec(self.deflt_func).args, getargspec(self.deflt_func).defaults)) defaults = (list(func.__defaults__) if func.__defaults__ is not None else []) func_args = reversed(getargspec(func).args[:-len(defaults)]) for func_arg in func_args: if func_arg not in defltargs: # Default arguments doesn't allow gaps, ignore rest break defaults.insert(0, defltargs[func_arg]) # Update list of default arguments func.__defaults__ = tuple(defaults) return funcdef f(accuracy=1e-3, nstep=10, b = 'bbb'): print ('f', accuracy, nstep, b)@use_defaults(f)def g(first, accuracy, nstep, a = 'aaa'): f(accuracy, nstep) print ('g', first, accuracy, nstep, a)g(True)g(False, 2e-5)g(True, nstep=32)
This however, rules out keyword-only arguments which have separate __kwdefaults__
, and probably blow up logic behind use_defaults
decorator.
You may also add arguments in runtime by using wrapper, but that will probably reduce performance.
My favorite, kwargs!
def f(**kwargs): kwargs.get('accuracy', 1e-3) ..def g(**kwargs): f(**kwargs)
Of course, feel free to use the constants as described above.