Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

From 'wrapping unbound' methods to 'aspect oriented' design

5 views
Skip to first unread message

Pedro Rodriguez

unread,
Jan 12, 2002, 7:11:14 PM1/12/02
to
Hello,

While trying to clean up some code I had, I noticed the following
problem : I had a class A with several methods m1, m2 ..., and
all those methods have the same "aspect" but not the best suited
to me. The code looked like :

class A:
...
def mx(self, ...):
self.notify("start")
... mx code here ...
self.notify("end")
...

and I wanted it to look like :

class A:
...
def mx(self, ...):
try:
self.notify("start")
... mx code here ...
self.notify("end")
except:
self.notify("error")
...

There was several methods were I had to do the modification, and
maybe this modification should also be applied to other classes :(
Probably there was a flow in my design, maybe there is something more
important I had missed while developping the first version.

I just happened to be looking at "aspect oriented" design (somebody
qualified this as something very promising in this newsgroup), and
I just realized that this was a typical example of what "aspects" are
targeted.

You may check the following refs :
- http://aosd.net
- http://aspectj.org
- http://aspectj.org/doc/papersAndSlides

and I will recommand (the easier understand IMHO):
- AspectJ tutorial
- http://aspectj.org/documentation/papersAndSlides/ECOOP1997-AOP.pdf


So the idea is to separate the "aspect" from the actual code in A,
and find a way to wrap methods m1, m2,... so that they are used through
the "aspect". But there was a constraint, the "aspect" should not be
intrusive in A code. This means that I had to wrap A's methods from
"outside" and not in through A's code.

[OT : I will use 1.5.2 syntax : apply(f, args, kwargs)
instead of the more convenient 2.x : f(*args, **kwargs)
]


If wrapping a bound method is easy:

class Wrap:
def __init__(self, boundMethod):
self.boundMethod = boundMethod

def __call__(self, *args, **kwargs):
print "before"
apply(self.boundMethod, args, kwargs)
print "after"

class A:
def __init__(self):
self.f = Wrap(self.f)

def f(self, x):
print x

a = A()
a.f("Hello world")


wrapping an unbound method was trickier (btw, python 2.2 doesn't
seem to provide a way to subclass the types build by the "new" module
like "instancemethod" ?)

class A:
def f(self, x):
print x


import new

class Wrap:
def __init__(self, cls, methodName):
self.unboundMethod = getattr(cls, methodName)
newMethod = new.instancemethod(self, None, cls)
setattr(cls, methodName, newMethod)

def __call__(self, *args, **kwargs):
print "before"
apply(self.unboundMethod, args, kwargs)
print "after"


Wrap(A, "f")

a = A()
a.f("Hello world")


NOTICE : this is the simplest way I found to wrap existing code
without changing anything to the base class. With this kind of
idioms, I may add as many wrappers I want :

class Counter(Wrap):
def __init__(self, cls, methodName):
Wrap.__init__(self, cls, methodName)
self.count = 0

def __call__(self, *args, **kwargs):
self.count = self.count + 1
print "called %d time(s)" % self.count
apply(self.unboundMethod, args, kwargs)

Counter(A, "f")
a.f("Hello World")


So the question is ... ? Am I overdoing ?
I know there may be some issues :
- cascading aspects may lead to some unspected behaviour (order ?)
- exception handling
- ...

I hope some of you may also found this interesting.
I will shortly post an Aspect implementation.

--
Pedro
PS : In AspectJ they call the aspect functions "advice",
but maybe I am the one to "add vice" to my code ;)

Pedro Rodriguez

unread,
Jan 13, 2002, 5:22:54 AM1/13/02
to
Here is an implementation of a (primitive-) Aspect.
It is not a full featured thing like AspectJ, and looks quite
dumb in regard with aspectr (from Ruby), but I found its simplicity
quite "pythonic".

There may be gotchas regarding how to nest advices, and a possible
bug with Python 2.2 (new.instancemethod refuses a new class object
as the third argument - will submit a bug just in case...).

So the idea is :
- you create an aspect class by subclassing Aspect
- you write your advices with the following signature
def an_advice(self, method, methodName, inst, *args, **kwargs):
where :
- self is the aspect
- method is the next advice to be called or the original method
- methodName is the name of the aspect-ified method
- inst is the object on which the method will be called
- args, kwargs are the arguments to the real method
- "method" should only be used for "around" advices, since they MUST
call your real method (or the next advice) :
apply(method, args, kwargs)
- "method" is useless for "before" and "after" advices
- you can then create an aspect to wrap any other class method with
anAspect.wrap_around


# Aspect implementation -----------------------------------------------

import new

class Aspect:
def wrap_around(self, adviceName, cls, methodName):
adviceMethod = getattr(self, adviceName)
AdviceAround(adviceMethod, cls, methodName)

def wrap_before(self, adviceName, cls, methodName):
adviceMethod = getattr(self, adviceName)
AdviceBefore(adviceMethod, cls, methodName)

def wrap_after(self, adviceName, cls, methodName):
adviceMethod = getattr(self, adviceName)
AdviceAfter(adviceMethod, cls, methodName)

class Advice:
def __init__(self, adviceMethod, cls, methodName):
self.methodName = methodName
self.adviceMethod = adviceMethod
self.origMethod = getattr(cls, methodName)
newMethod = new.instancemethod(self.do_aspect, None, cls)
setattr(cls, methodName, newMethod)

def do_advice(self, *args, **kwargs):
inst = args[0]
apply(self.adviceMethod \
, (self.origMethod, self.methodName, inst) + args
, kwargs
)

def do_method(self, *args, **kwargs):
apply(self.origMethod, args, kwargs)

def do_aspect(self, *args, **kwargs):
raise NotImplementedError

class AdviceAround(Advice):
def do_aspect(self, *args, **kwargs):
apply(self.do_advice, args, kwargs)

class AdviceBefore(Advice):
def do_aspect(self, *args, **kwargs):
apply(self.do_advice, args, kwargs)
apply(self.do_method, args, kwargs)

class AdviceAfter(Advice):
def do_aspect(self, *args, **kwargs):
apply(self.do_method, args, kwargs)
apply(self.do_advice, args, kwargs)

# Sample code ---------------------------------------------------------

class A:
def f(self, x, y):
print "real f %s %s" % (x, y)

def g(self, x):
print "real g %s" % x

class B:
def z(self, *args, **kwargs):
print "real z %s %s" % (str(args), str(kwargs))


class MyAspect(Aspect):
def around_advice(self, method, methodName, inst, *args, **kwargs):
print "start"
apply(method, args, kwargs)
print "stop"

def before_advice(self, method, methodName, inst, *args, **kwargs):
print ">>>>>>>>>"
print "before", methodName, inst, args, kwargs

def after_advice(self, method, methodName, inst, *args, **kwargs):
print "after", methodName, inst, args, kwargs
print "<<<<<<<<<"

aspect = MyAspect()
aspect.wrap_around("around_advice", A, "f")
aspect.wrap_before("before_advice", A, "g")

aspect.wrap_before("before_advice", B, "z")
aspect.wrap_after("after_advice", B, "z")


a = A()
a.f(1,2)
a.g(3)

print

b = B()
b.z(1,2,3, x=42)
b.z(4)

-----------------------------------------------------------------------
This fails with Python 2.2 :
...
newMethod = new.instancemethod(self.do_aspect, None, cls)
TypeError: instancemethod() argument 3 must be class, not type
-----------------------------------------------------------------------

class A(object):
def f(self, x):
print "real f", x

aspect.wrap_around("around_advice", A, "f")
a = A()
a.f(1)


--

Pedro

0 new messages