2
\$\begingroup\$

I wanted to share this and get some feedback. My goal was to be able to have an object with methods:

class Thing(object):
    def __init__(self, data=123):
        self.data = data

    def add_one(self):
        self.data += 1

    def add_number(self, number):
        self.data += number

And then a collection object, MetaThing that can hold several Thing objects, which inherits from Thing like this:

class MetaThing(Thing):

    def __init__(self):
        super(MetaThing, self).__init__()

        self.list = [1, 2, 3, 4, 5]
        self.things = []
        self.setupThings()

    def setupThings(self):

        for i in self.list:
            thing = Thing(data=i)
            self.things.append(thing)

Finally, I want to be able to apply the methods in Thing to all the Thing instances stored in MetaThing.things when those same methods are called on MetaThing.

Doing this explicitly creates methods like this:

    def add_one(self):
        for t in self.things:
            t.add_one()

    def add_number(self, number):
        for t in self.things:
            t.add_number(number)

So I started wondering if this would be a good situation for a decorator, so I wrote one that I think does what I want:

def allthings(func):
    def new_func(*args, **kwargs):
        self = args[0]
        for thing in self.things:
            arglist = list(args)
            arglist[0] = thing
            func(*arglist, **kwargs)

    return new_func

So now, the MetaThing method can use the @allthings decorator to call an inherited method on all child objects without rewriting the for loop every time:

class MetaThing(Thing):

    def __init__(self):
        super(MetaThing, self).__init__()

        self.list = [1, 2, 3, 4, 5]
        self.things = []
        self.setupThings()

    def setupThings(self):

        for i in self.list:
            thing = Thing(data=i)
            self.things.append(thing)

    @allthings
    def add_one(self):
        self.add_one()

    @allthings
    def add_number(self, number):
        self.add_number(number)

Finally this block verifies that it's working:

if __name__ == '__main__':

    meta = MetaThing()

    meta.add_one()

    print [t.data for t in meta.things]

    meta.add_number(5)

    print [t.data for t in meta.things]

Is this a valid approach? Is there a better way to achieve this? Am I crazy for using decorators in this way?

My actual scenario is a PlottedShape with lots of methods like plot, delete, update, select, apply_opacity, apply_color, apply_wireframe, etc. and I also have a PlottedMultiShape made up of multiple shapes (like a snowman shape made of multiple circles). If PlottedMultiShape.select() is called I want to call select on all the child PlottedShape objects -- and I'd like this same behavior for all of the other methods as well. Using a decorator seemed like a good "Don't Repeat Yourself" tactic.

\$\endgroup\$
2
  • \$\begingroup\$ It is probably a mistake to use an inheritance relationship between Thing and MetaThing in the first place. It's a "MetaThing contains Things" relationship, not a "MetaThing is a Thing" relationship. I can't say for certain what is appropriate, though, since Thing is a purely hypothetical example. \$\endgroup\$ Dec 18, 2015 at 22:44
  • \$\begingroup\$ In this case I was trying to say that the same verbs apply to both, that one is simply the plural of the other... like if I wrote Word.capitalize() for some reason, could I get Phrase_of_Words.capitalize() to automatically do for word in phrase: word.capitalize().... and do this also for bold(), underline(), etc. I see what you mean though... having ThingCollection which owns a list of Things would be more explicit (Composition instead of Inheritance I think?) and could still handle the method proxy/broadcasting behavior. \$\endgroup\$ Dec 18, 2015 at 22:59

1 Answer 1

1
\$\begingroup\$

Another way to do this would be to use __getattr__().

class Wraps(object):
  def __init__(self, *args):
    self.things = args
  def __getattr__(self, item):
    def each(*args, **kwargs):
      for thing in self.things:
        getattr(thing, item)(*args, **kwargs)
    return each
  def some_real_wraps_method(self):
    print "real method"

class Thing(object):
  def __init__(self, name):
    self.name = name
  def a(self):
    print self.name, "a"
  def b(self, *args, **kwargs):
    print self.name, "b", args, kwargs

w = Wraps(Thing("1"), Thing("2"))
w.a()
w.b()
w.b("arg1", "arg2", named=True)
w.some_real_wraps_method()

Running this will output:

1 a
2 a
1 b () {}
2 b () {}
1 b ('arg1', 'arg2') {'named': True}
2 b ('arg1', 'arg2') {'named': True}
real method

The upside is that you don't have to define an methods in order for them to be proxied. The downside is that it will try to proxy any attribute not defined by Wraps (this can be counteracted with a white list).

\$\endgroup\$
1
  • \$\begingroup\$ yeah I tried to get something similar to work by having a list of methods to wrap and then doing like setattr(self, method, allthings(getattr(self, method))) but it wasn't passing self correctly or something. Didn't think to just override __getattr__. \$\endgroup\$ Jun 24, 2014 at 22:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.