Is there are good DRY fix for this painful design pattern?

Peter Otten __peter__ at web.de
Mon Feb 26 11:39:43 EST 2018


Steven D'Aprano wrote:

> I have a class with a large number of parameters (about ten) assigned in
> `__init__`. The class then has a number of methods which accept
> *optional* arguments with the same names as the constructor/initialiser
> parameters. If those arguments are None, the defaults are taken from the
> instance attributes.
> 
> An example might be something like this:
> 
> 
> class Foo:
>     def __init__(self, bashful, doc, dopey, grumpy,
>                        happy, sleepy, sneezy):
>         self.bashful = bashful  # etc
> 
>     def spam(self, bashful=None, doc=None, dopey=None,
>                    grumpy=None, happy=None, sleepy=None,
>                    sneezy=None):
>         if bashful is None:
>             bashful = self.bashful
>         if doc is None:
>             doc = self.doc
>         if dopey is None:
>             dopey = self.dopey
>         if grumpy is None:
>             grumpy = self.grumpy
>         if happy is None:
>             happy = self.happy
>         if sleepy is None:
>             sleepy = self.sleepy
>         if sneezy is None:
>             sneezy = self.sneezy
>         # now do the real work...
> 
>     def eggs(self, bashful=None, # etc...
>                    ):
>         if bashful is None:
>             bashful = self.bashful
>         # and so on
>  
> 
> There's a lot of tedious boilerplate repetition in this, and to add
> insult to injury the class is still under active development with an
> unstable API, so every time I change one of the parameters, or add a new
> one, I have to change it in over a dozen places.
> 
> Is there a good fix for this to reduce the amount of boilerplate?

I have not yet looked into dataclasses. Don't they handle the __init__() 
part? Anyway, here's my attempt to make spam() less spammy:

$ cat attrs_to_args_decorator.py     
import functools
import inspect

def add_defaults(f):
    argnames = inspect.getfullargspec(f).args[1:]

    @functools.wraps(f)
    def wrapper(self, *args, **kw):
        args = [
            getattr(self, name) if value is None else value
            for name, value in zip(argnames, args)
        ]
        for name in argnames[len(args):]:
            if name not in kw or kw[name] is None:
                kw[name] = getattr(self, name)
        return f(self, *args, **kw)
    return wrapper


def update_attrs(kw):
    self = kw.pop("self")
    for name, value in kw.items():
        setattr(self, name, value)


class Foo:
    def __init__(self, bashful, doc, dopey, grumpy, 
                       happy, sleepy, sneezy):
        update_attrs(locals())

    @add_defaults
    def spam(self, bashful=None, doc=None, dopey=None, 
                   grumpy=None, happy=None, sleepy=None,
                   sneezy=None):
        return "{}-{}-{}".format(bashful, doc, sneezy)

    def __repr__(self):
        return "Foo({})".format(
            ", ".join(
                "{}={!r}".format(*pair)
                for pair in sorted(self.__dict__.items())
            )
        )

if __name__ == "__main__":
    foo = Foo("bashful", "doc", "dopey", "grumpy", "happy", "sleepy", 
"sneezy")
    print(foo)
    print(foo.spam())
    print(foo.spam(bashful="BASHFUL"))
    print(foo.spam(bashful="BASHFUL", doc="DOC"))
    print(foo.spam("BASHFUL"))
    print(foo.spam("BASHFUL", "DOC"))
    print(foo.spam("BASHFUL", "DOC", sneezy="SNEEZY"))
$ python3 attrs_to_args_decorator.py 
Foo(bashful='bashful', doc='doc', dopey='dopey', grumpy='grumpy', 
happy='happy', sleepy='sleepy', sneezy='sneezy')
bashful-doc-sneezy
BASHFUL-doc-sneezy
BASHFUL-DOC-sneezy
BASHFUL-doc-sneezy
BASHFUL-DOC-sneezy
BASHFUL-DOC-SNEEZY
$




More information about the Python-list mailing list