This post originated from an RSS feed registered with Python Buzz
by Ben Last.
Original Post: Calling Occupants
Feed Title: The Law Of Unintended Consequences
Feed URL: http://benlast.livejournal.com/data/rss
Feed Description: The Law Of Unintended Consequences
I've just been faced with adding appropriate superclass calls to eight functions of a Zope product. The main class for the product inherits from three rather important superclasses that all need to have their various manage_ methods poked appropriately. So, inevitably, rather than hardwire in a whole load of calls (which could be done and finished in ten minutes) I choose to invest a while in looking at ways to solve the general problem (which takes a lot longer but which is more fun). Thus the inner nature of the geek is shown.
Let's construct a context for this; non-co-operative superclasses thus:
class A:
def meth(self):
print "A"
class B:
def meth(self):
print "B"
Now let's write a class C that calls the superclasses. Here's the first pass:
class C(A,B):
def meth(self):
for b in self.__class__.__bases__:
if hasattr(b,'meth'):
b.meth(self)
print "C"
That's ok, but there're interesting optimizations to be applied, some of which are even worth doing (some of which are just for fun). Second pass:
class C(A,B):
def meth(self):
for b in [x for x in self.__class__.__bases__ if hasattr(x,'meth')]:
b.meth(self)
print "C"
Nicer, in that it gives us an excuse to use a list comprehension. However, we're running that whole for, including the expensive hasattr for every call, which offends my sense of efficiency. We could omit the hasattr and use a try: except AttributeError[1], but I'm assuming that we'd use this where we expect the method to exist. Caching is what's wanted:
class C(A,B):
superdict={}
def meth(self):
try:
#retrieve cached list of method objects
methods = self.__class__.superdict['meth']
except KeyError:
#oops, load the cache
methods = [getattr(x,'meth') for x in self.__class__.__bases__ if hasattr(x,'meth')]
self.__class__.superdict['meth'] = methods
#call any superclass methods in appropriate order
for method in methods: method(self)
print "C"
So here we keep a dict that maps a method name to a list of superclass methods (as objects). I've left it as a first change from the previous one to show the reasoning, but those hardwired 'meth' constants bug me even now, so let's zap them away. And whilst we're at it, we'd better generalise the arguments a bit too, and make sure we use only callables.
class C(A,B):
superdict={}
def callsuper(self, name, *args, **kwargs):
try:
#retrieve cached list of method objects
methods = self.__class__.superdict[name]
except KeyError:
#load the cache.
methods=[]
for x in self.__class__.__bases__:
m = getattr(x,name,None)
if callable(m): methods.append(m)
self.__class__.superdict[name] = methods
#call any superclass methods in appropriate order.
for method in methods:
method(*[self]+list(args), **kwargs)
def meth(self):
self.callsuper('meth')
print "C"
Note this uses the Python 2 extended call syntax rather than apply().
There's still one source of error; we have to remember to change the method name parameter for every call to callsuper. To make it extra-super-clever, look at this redefined version:
def callsuper(self, *args, **kwargs):
#derive the name of the calling function.
frame = inspect.currentframe(1)
name = frame.f_code.co_name
try:
#retrieve cached list of method objects
methods = self.__class__.superdict[name]
except KeyError:
#load the cache.
methods=[]
for x in self.__class__.__bases__:
m = getattr(x,name,None)
if callable(m): methods.append(m)
self.__class__.superdict[name] = methods
#call any superclass methods in appropriate order.
for method in methods:
method(*[self]+list(args), **kwargs)
Here, rather than pass in the name of the method, we use the inspect module to derive it by looking down the stack frame to get the name of the method that called callsuper. Thus our meth method becomes:
def meth(self):
self.callsuper()
print "C"
And that's enough exotic stuff for one day.
[1] Actually, according to at least one source, hasattr is implemented using a getattr and a try: block, so it's no more efficient.