Avoiding code repetition in default arguments in Python Avoiding code repetition in default arguments in Python python python

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.