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