Reputation: 2479
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
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
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
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