aspect-oriented demo using metaclasses

Pedro RODRIGUEZ pedro_rodriguez at club-internet.fr
Sat Jun 29 15:27:52 EDT 2002


On Fri, 28 Jun 2002 17:31:06 +0200, Mark McEahern wrote:

<snip>

> I owe much to previous threads on AOP, in particular Pedro Rodriguez's
> non-metaclass implementation:
> 

Mark,

If you really want something more consistent than my posting, I will
recommend you to also check:

    - Pythius by Juergen Hermann
        where Frank J. Tobin has introduced an aop module
        http://pythius.sourceforge.net/ by Juergen Hermann

    - Transwarp by Phillip J. Eby
        this a more complex framework using in some way concepts of aop
        http://telecommunity.com/TransWarp/
        http://www.zope.org/Members/pje/Wikis/TransWarp/HomePage


<snipping introduction on metaclass>

> 
> We want to iterate through the class's dict and do something for each
> method (I'm not worried about distinguishing staticmethod and
> classmethod type methods for now):
> 

You should <wink>. At least for the purpose of the exercise ;)
(Honestly I didn't try either, it may end up with a two liner...
but I wonder if they will take two minutes or two hours to be written ;)


<sniping>

> Rather than importing types and using types.FunctionType, I just compare
> the type of the item to the type of an anonymous function (via lambda).
> Is that cool or what?  ;-)
> 

Even if deprecation of the types module have been discussed on the devel
list, I think it is preferable.


<sniping>

> Now we're starting to get somewhere!  The next step is to create a
> framework where multiple observers can plug into the before and after
> events for any given method call.  Rather than taking more baby steps to
> get there, this is the complete implementation I have so far:
> 

You went even further than that. In the next step, you start separating
(at least ;) the aspect and the 'aspectified' object in a less intrusive 
way.


> <code>
> import sys
> 
> class wrapped_method(object):
> 
>     def __init__(self, cls, method):
>         self.class_name = cls.__name__
>         self.method = method
>         self.method_name = method.func_name
>         self.observers = []
> 
>     def __call__(self, *args, **kwargs):
>         self.before(*args, **kwargs)
>         self.method(self, *args, **kwargs)
>         self.after(*args, **kwargs)
> 

Oops... Didn't I make the same mistake... 
... forgetting about the returned value ;)

Something like could be better:
     def __call__(self, *args, **kwargs):
         self.before(*args, **kwargs)
         ret = self.method(self, *args, **kwargs)
         self.after(*args, **kwargs)
         return ret
 

<sniping>

> class Trace(Observer):
> 
>     def __init__(self, filename=None):
>         self.filename = filename
> 
>     def write(self, prefix, class_name, method_name, *args, **kwargs):
>         cls = class_name
>         s = "%s: %s.%s(%s, %s)" % (prefix, cls, method_name, args,
>         kwargs) if not self.filename:
>             f = sys.stdout
>         else:
>             f = file(self.filename)
>         f.write(s)
>         f.write("\n")
>         if self.filename:
>             f.close()
> 
>     def before(self, class_name, method_name, *args, **kwargs):
>         self.write("before", class_name, method_name, *args, **kwargs)
> 
>     def after(self, class_name, method_name, *args, **kwargs):
>         self.write("after", class_name, method_name, *args, **kwargs)
> 

This Trace class is an aspect.


> class Aspect(type):
> 
>     def __init__(cls, name, bases, dict):
>         super(Aspect, cls).__init__(name, bases, dict) for k,v in
>         dict.items():
>             if type(v) == type(lambda x:x):
>                 setattr(cls, k, wrapped_method(cls, v))
> 

This is not an Aspect. This is your way to implement method call interception.


<sniping>

> ************
> Observations
> ************
> 
> Obviously, Trace is designed so that I can specify a filename when
> creating an instance of it and that would use that file rather than
> sys.stdout.
> 
> Observers should probably be able to register for particular events
> (i.e., just before).  They should be able to unregister.  It should also
> be possible to register for some classes, some methods, and not others.

What about module functions ? Bounded methods ?
Using metaclass for call interception is too restrictive I believe.


> What is a good way to write the rules for enrolling in notifications?
> (And what's the word for this that AspectJ uses?)

Joinpoint... but you already found it ;)


> 
> I explicitly pass self from the wrapped_method instance to the actual
> wrapped method.  However, this is not really the instance of the class,
> it's the instance of the wrapped_method object.  How do I pass the
> actual instance to the wrapped method--whether explicitly or implicitly?
> 

This is the trickier part. When you do :
       setattr(cls, k, wrapped_method(cls, v))
you substitute a function (actually an unbounded method) by a callable
object. Unfortunately when you'll invoke this object with a classical
method call, Python will not pass the instance as the first argument.

I only know two ways to substitute an unbounded method by an object
that will allow to retrieve the instance argument :
- providing a function, and take benifit of nested_scopes to retrieve
  all the information from your context
- create an object through the 'new' package (new.instancemethod)
  that was broken for new type objects in 2.2, and fixed in 2.2.1

So your wrapped_method class is not sufficient per se to achieve this
goal.


> I should probably explicitly set the metaclass for wrapped_method,
> Trace, Observer, and even VirtualClassError to "type" (the default
> metaclass) to avoid weird infinite loops where the members of the aspect
> framework are themselves being aspected.
> 

I don't think that metaclass'ing is the good thing to do, but that's
just me.


> I need to think more about how this would be used.  My vague impression
> of AspectJ is that you use it from an IDE, specifying different
> conditions for filtering classes and methods (what's the word for that
> again?).  When you compile with aspect support turned on, the generated
> code has the aspects weaved in.  What are the equivalent developer use
> cases for weaving aspects into Python code? The whole point it seems to
> me is that when I author the classes to be aspected, I shouldn't have to
> do a thing--no subclassing, no method definition.  I just write my
> classes, omitting entirely any consideration of the future aspects to be
> weaved in.  I mean, that's the whole point, right?  To simplify the
> code.
> 

Yep. And this is why I consider the metaclass thing for being too intrusive.


> In the example, foo doesn't know diddly about the fact that it's being
> traced.  And we could turn that tracing on or off simply by changing the
> module-level __metaclass__ variable.
> 

Too intrusive. I don't believe that you can do it dynamically, at least
not for classes defined at module level. They will be created with the
metaclass defined at compilation time.


<sniping>

> It would be easy to add aspects for error handling.  This is a very
> quick and dirty example for error notification:
> 

<snip>

Yes. Interception of raised exception is a good (and easy ;) feature.
Just try to go a step further with 'around' methods.


Thanks, for this posting Mark. Reminds me that aop is my task list for
the moment I will have some spare time (not so far I hope ;)

Just to give some hints on aop, this is what I wrote as a reminder
6 months ago along with a more complete implementation of aop :


    Vocabulary

    pointcut
    an object identifying a set of joinpoints, and aggregating a set of
    advices

    joinpoint
    a callable function in Python (unbound method, bound method, function)

    advice
    a function that will be called when a joinpoint is reached. It may be
    triggered before the joinpoint is called, after, or when an exception
    occurs.
    An advice may also be set around a joinpoint, meaning that it will be
    able to do some job before and after the joinpoint runs, but it will
    have to call the joinpoint himself.

    aspect
    a class that contains poincuts and their related advices.


And a UML'ish model :
+-------------+       n  +-------------+       1 +-------------+
| PointCut    | - - - -> |  JoinPoint  |-------->|   Function  +
+-------------+          +-------------+         +-------------+
      |                    |
      |                    |
      V  1                 |
+-------------+  n         |
| AdviceStack |<-----------+
+-------------+
      |
      |
      V  n
+-------------+
|   Advice    |
+-------------+

- A pointcut uses several joinpoints
- Since a joinpoint may participate in several pointcuts, it will have
  to trigger several set of advices (AdviceStack) related to each of the
  pointcut it belongs to.
- A joinpoint is related to a Python function. It will intercept calls
  to it.



+-------------+         1 +-------------+
|  JoinPoint  |---------->|   Function  |
+-------------+           +-------------+
                                 ^ n
                                 |
                                 |
                          +-------------+
                          | CallableSet |
                          +-------------+
                                 A
                                 |
       +-----------------+-------+---------+----------------+
       |                 |                 |                |
+--------------+  +-------------+   +-------------+  +-------------+
|ClassicalClass|  |  TypeClass  |   |    Module   |  |  Instance   |
+--------------+  +-------------+   +-------------+  +-------------+

- A Python function related to a joinpoint belongs to a CallableSet.
- A CallableSet can be a classical python class, in which case the
  function is an unbound method.
- A CallableSet can be a new python class, in which case the function
  is an unbound method.
- A CallableSet can be a python instance, in which case the function
  is a bound method.
- A CallableSet can be a python module, in which case the function
  is a standard method.


Best regards,
Pedro




More information about the Python-list mailing list