Paul
Paul

Reputation: 2479

Python: How do you chain methods on lists of lists of objects?

I want be able to chain methods on a node instance that has children and grandchildren nodes. When I call a method on that top node, I want to be able to return the children and grandchildren of that object. I also want to be able to get attributes of those children and grandchildren from the very top node. I have the following data model:

class GrandParent(object):
    def __init__(self,name='',age=0,is_retired=True,children=[]):
        self.name = name
        self.age = age
        self.is_retired = is_retired

        if children:
            self.children = children

    def print_mychildren(self):
       for child in self.children:
            print "Child: ",child

    def mychildren(self):
        return self.children

    def __str__(self):
        return "name: %s age: %s is_retired:%s" %(self.name,self.age,self.is_retired)


class Parent(object):
    def __init__(self,name='',age=0,has_mortgage=True,children=[]):
        self.name = name
        self.age = age
        self.has_mortgage = has_mortgage

        if children:
            self.children = children

    def print_mychildren(self):
       for child in self.children:
            print "Child: ",child

    def __str__(self):
        return "name: %s age: %s has_mortgage: %s" %(self.name,self.age,self.has_mortgage)


class Child(object):
    def __init__(self,name='',age=0,has_toys=True):

        self.name = name
        self.age = age
        self.has_toys = has_toys

    def __str__(self):
        return "name: %s age: %s has_toys:%s" %(self.name,self.age,self.has_toys)



if __name__ == '__main__':

    beaver = Child('Beaver',12,True)
    ward = Child('Ward',16,False)

    june = Parent('June',38,True,[beaver,ward])

    grandpa = GrandParent('Grandpa',61,True,[june])

    print grandpa

    grandpa.print_mychildren() # print June

    # Doesn't work
    grandpa.mychildren().print_mychildren() #  I want it to print Ward and Beaver
    # Doesn't work
    print grandpa.mychildren().mychild('Beaver').age # I want it to return an age

Please note that I want to keep GrandParent, Parent, and Child classes separate because I want to give different attributes to each class such as has_mortgage or is_retired.

From the above data model, I'd like to be able to chain the methods so as to traverse the children of the top node. It would look like this:

grandpa.children # returns a list of children
print grandpa.mychild('June').has_mortgage # returns the value
grandpa.mychildren().mychildren() # prints a list of grandchildren
print grandpa.mychildren().mychild('Ward').has_toys # return the value

In other words, can I make the statement:

print grandpa.mychildren().mychildren()

behave like:

for child in grandpa.children:
        for grandchild in child.children:
            print grandchild

I would appreciate your answers to this. Thank you.

Paul
Chicago, IL

Upvotes: 1

Views: 719

Answers (3)

nehz
nehz

Reputation: 2182

One way is to create a wrapper for the list collection (slightly similar to how jQuery handles method chaining)

Code:

class ChainList(list):
    def __getattribute__(self, name):
        try:
            return object.__getattribute__(self, name)
        except:
            pass

        return ChainList([getattr(child, name) 
            for child in self if name in dir(child)])

    def __call__(self, *args, **kwargs):       
        return ChainList([child(*args, **kwargs)
            for child in self if callable(child)])

class GrandParent(object):
    def __init__(self,name='',age=0,is_retired=True,children=[]):
        self.name = name
        self.age = age
        self.is_retired = is_retired

        if children:
            self.children = ChainList(children)

    def print_mychildren(self):
       for child in self.children:
            print "Child: ",child

    def mychildren(self):
        return self.children

    def __str__(self):
        return "name: %s age: %s is_retired:%s" %(self.name,self.age,self.is_retired)

class Parent(object):
    def __init__(self,name='',age=0,has_mortgage=True,children=[]):
        self.name = name
        self.age = age
        self.has_mortgage = has_mortgage

        if children:
            self.children = ChainList(children)

    def print_mychildren(self):
       for child in self.children:
            print "Child: ",child

    def __str__(self):
        return "name: %s age: %s has_mortgage: %s" %(self.name,self.age,self.has_mortgage)

    def mychild(self, name):
        for child in self.children:
            if child.name == name:
                return child

class Child(object):
    def __init__(self,name='',age=0,has_toys=True):

        self.name = name
        self.age = age
        self.has_toys = has_toys

    def __str__(self):
        return "name: %s age: %s has_toys:%s" %(self.name,self.age,self.has_toys)



if __name__ == '__main__':

    beaver = Child('Beaver',12,True)
    ward = Child('Ward',16,False)

    june = Parent('June',38,True,[beaver,ward])

    grandpa = GrandParent('Grandpa',61,True,[june])

    print grandpa

    grandpa.print_mychildren() # print June

    # Doesn't work
    grandpa.mychildren().print_mychildren() #  I want it to print Ward and Beaver
    # Doesn't work
    print grandpa.mychildren().mychild('Beaver').age # I want it to return an age

The main idea is the ChainList class and overriding __getattribute__ and __call__ and wrapping all lists that needs to be chained with this class.

I also added mychild() to Parent to make the example work.

Upvotes: 1

Blckknght
Blckknght

Reputation: 104852

You can do what you want, but it will require a bit of work. You'll need to make the children value of your Parent and Grandparent classes a custom container that maps accesses to methods and member variables to its contents.

Here's a possible implementation. It may have some weird corner cases that I haven't sorted out, but it works for the basic stuff I've tried:

from operator import attrgetter

class mappingContainer(object):
    def __init__(self, contents):
        self.contents = contents

    # sequence protocol methods    
    def __getitem__(self, index):
        return self.contents[index]

    def __setitem__(self, index, value):
        self.contents[index] = value

    def __iter__(self):
        return iter(self.contents)

    def __contains__(self, o):
        return o in self.contents

    def __len__(self):
        return len(self.contents)

    # map attribute access and method calls
    def __getattr__(self, name):
        return mappingContainer(map(attrgetter(name), self.contents))

    def __call__(self, *args, **kwargs):
        return mappingContainer([o(*args, **kwargs) for o in self.contents])

    # string conversions
    def __repr__(self):
        return "mappingContainer(%s)" % repr(self.contents)

Here's a simple "Person" class that I used to test it out. Each person has a name, zero or more children in a mappingContainer and a single arbitrarily named method that prints something and returns a value.

class Person(object):
    def __init__(self, name, children = None):
        self.name = name
        if not children:
            children = []
        self.children = mappingContainer(children)

    def __repr__(self):
        return self.name

    def someMethod(self):
        print "%s's someMethod()" % str(self)
        return "%s's return value" % str(self)

Here's how I tested it (with long lines wrapped, for showing here):

>>> c1 = Person("Adam")
>>> c2 = Person("Albert")
>>> c3 = Person("Alice")
>>> c4 = Person("Agnes")
>>> p1 = Person("Bob", [c1, c2])
>>> p2 = Person("Betty", [c3,c4])
>>> gp = Person("Charles", [p1, p2])
>>> gp
Charles
>>> gp.children
mappingContainer([Bob, Betty])
>>> gp.children.children
mappingContainer([mappingContainer([Adam, Albert]),
                  mappingContainer([Alice, Agnes])])
>>> gp.children.children.someMethod()
Adam's someMethod()
Albert's someMethod()
Alice's someMethod()
Agnes's someMethod()
mappingContainer([mappingContainer(["Adam's return value",
                                    "Albert's return value"]),
                  mappingContainer(["Alice's return value",
                                    "Agnes's return value"])])

Now this may not be quite what you want, since you can't directly iterate over the grandchildren via gp.children.children (and though function calls will bubble all the way down, their results will still be nested in two layers of containers). I think the mappingContainer might be adjusted in some way to deal with that, but I'm not sure it is worth it.

Chained calls of this sort are going to be a nightmare to debug if anything goes wrong (and unless you're superhuman, something in your code will always go wrong at some point in development). Your code will be much easier to write and understand if you explicitly iterate or recurse through the hierarchy of relationships you have.

Upvotes: 2

schacki
schacki

Reputation: 9533

To me this sounds very much like you were talking about models and model relations. Have a look at Django. Django Models will do exactly what you want and you can define relations between models and query across those relations.

Upvotes: 0

Related Questions