How to reduce the DRY violation in this code

Chris Angelico rosuav at gmail.com
Tue Sep 27 12:39:28 EDT 2016


On Wed, Sep 28, 2016 at 1:49 AM, Steve D'Aprano
<steve+python at pearwood.info> wrote:
>     @classmethod
>     def from_strings(cls, bashful='10.0', doc='20.0', dopey='30.0',
>                      grumpy='40', happy='50', sleepy='60', sneezy='70'):
>         bashful = float(bashful)
>         doc = float(doc)
>         dopey = float(dopey)
>         grumpy = int(grumpy)
>         happy = int(happy)
>         sleepy = int(sleepy)
>         sneezy = int(sneezy)
>         return cls(bashful, doc, dopey, grumpy, happy, sleepy, sneezy)
>
>
> That's a pretty ugly DRY violation. Imagine that I change the default value
> for bashful from 10.0 to (let's say) 99. I have to touch the code in three
> places (to say nothing of unit tests):
>
> - modify the default value in __init__
> - modify the stringified default value in from_strings
> - change the conversion function from float to int in from_strings
>
>
> Not to mention that each parameter is named seven times.
>

You could go data-driven. Assuming that all your default values are in
the appropriate type (eg you use 10.0 rather than 10, when you want a
float), you could use those directly.

class Spam:
    def __init__(self, bashful=10.0, doc=20.0, dopey=30.0,
                 grumpy=40, happy=50, sleepy=60, sneezy=70):
        for name, default in zip(self.__init__.__defaults__,
                self.__init__.__code__.co_varnames[1:]):
            setattr(self, name, type(default)(locals()[name]))

Your basic __init__ method is now capable of handling strings as well,
so from_strings can simply construct the object directly. I'm not sure
what the advantage of from_strings is, but assuming you still need it,
you could write it thus:

    @classmethod
    def from_strings(cls, bashful, doc, dopey, grumpy, happy, sleepy, sneezy):
        return cls(bashful, doc, dopey, grumpy, happy, sleepy, sneezy)
    from_strings.__func__.__defaults__ = tuple(str(x) for x in
__init__.__defaults__)

No duplication of type names or default values, though there is still
duplication of parameter names. You could eliminate that by going
*args,**kw, but at the expense of introspectability. Actually, you
could probably just use wraps...

    @classmethod
    @functools.wraps(__init__, assigned=())
    def from_strings(cls, *a, **kw): return cls(*a, **kw)
    from_strings.__func__.__defaults__ = tuple(str(x) for x in
__init__.__defaults__)

Though I'm not particularly enamoured of this way of doing it.

ChrisA



More information about the Python-list mailing list