A Light-weight Approach to Aspect Oriented Programming in Python

Introduction

This introduces a Python module which implements one essential concept in Aspect Oriented Programming (AOP).

Aspect Oriented Programming (AOP) extends Object Oriented Programming so that it allows the programmer to separate properties which are the same regardless of the classes they belong to. In AOP these properties are implemented only once in one place and then merged to the class structure everywhere they belong. For example, validating an SQL query and writing a log entry may be properties which do not need to know where they are executed --- so why write them all over the class structure?

In this document a light-weight solution to this problem is presented in Python. The solution is a function which takes an unbound method and another function (also known as advice) as its parameters. The function wraps the method inside the advice. Now when the method is called, the advice receives the control and it may, if it wishes, pass the control to the original method at some point of its execution.

The aspects module provides a function called wrap_around. The function takes two arguments:

  1. an unbound method m (that is a method of a class, not a method of an instance of a class) which will be wrapped into
  2. a function f, which is called advice in AOP terminology.

wrap_around modifies the class of the unbound method so that when m is called, execution jumps actually to f. If self.__proceed method is called in f, the original method is executed.

In AOP terminology, advice f is applied to a pointcut in around fashion. The pointcut includes exactly one join point: the call of method m.

Say it in Python, please

For example, consider class c:

class c:
    def foo(self):
        print "foo"

and function adv:

def adv(self, *args, **keyw):
    print "before"
    rv = self.__proceed(*args,**keyw)
    print "after"
    return rv

You can wrap the method foo of class c inside function adv by calling:

wrap_around( c.foo, adv )

Now, executing

o = c()
o.foo()
    

results output

before
foo
after
    

Implementation

When wrap_around is called with a method of a class and a function as its parameters, wrapping is implemented in the following way:

  1. It is ensured that __proceed-method exists in the class. If there is no such method, then it is plugged into the class.
  2. The wrapped method foo is renamed to __wrappedifoo, where i is the number of wraps around the method before wrapping it. Initially i==0.
  3. The advice is plugged into the class by name __wrapifoo, where i is the same as in the previous step.
  4. A new method is created and plugged into the class. The method is given the name of the original method. The new method pushes __wrappedifoo to __proceed_stack and calls __wrapifoo. When the call finishes, the method pops the element from __proceed_stack and returns the return value of __wrapifoo.
  5. After plugging the new method, i is incremented and stored to __wrapcountfoo-attribute.

In the Python code of the previous section, the function calls caused by line o.foo() are:

c.foo           # this is the new method created by wrap_around
c.__wrap0foo    # this is actually function "adv"
c.__proceed     # this function is called by "adv", the code is in
                # aspects.py
c.__wrapped0foo # this is the original method "foo"

The implementation is in aspects.py

Examples

Tracer

A tracer advice is defined in the tracer_advice.py module. The advice prints the name of the wrapped method, the class of the method and the class of the instance whose method is called. Then the advice passes control to the wrapped method and finally returns the return value of the wrapped method.

The tracer is used in the tracer_example.py module. This example demonstrates how the same advice can be used in several classes and around several methods. It also shows that a method can be wrapped in a derived class DD1 even if it is defined and already wrapped in its base class.

In this situation some attention should be paid to the order in which the methods are wrapped in the classes. If the method is wrapped first in the derived class and there are no wraps around methods of its base classes, then the proceed stack is created into the derived class. Now, if the same method of the base class is wrapped later on, the wrap in the base class will not be executed. Therefore, it is better to wrap first the methods of the base classes and then the methods of the derived classes.

Timeout

Timeout advice generator create_timeout_advice is given in timeout_advice.py. The generator takes two optional parameters:

  1. time to wait before timeout in seconds, the default is 1.0
  2. a value which is

The generator returns a function that can be used as an advice in wrap_around calls.

Assume that timeout advice t is wrapped around method m. When m is called, t receives control. It initialises its timer and starts a new thread. The thread calls __proceed and thus passes control to the original method. When the method finishes, the thread saves the return value to a variable which can be accessed by the timeout advice.

In the meantime, the timeout advice waits for the thread to finish or time to run out. If time runs out, t returns None and leaves the thread running on its own. If the thread finishes before the timeout, t returns the value saved by the thread.

There are two examples where a timeout advice is used. First, timeout_example.py is a simple example that demonstrates how the timeout advice works when catching exceptions.

The second example is a more sophisticated, ``real-life'' program httpget.py. This program wraps timeout aspect around open method of URLopener class. The class is located in urllib which is a part of the standard library included in Python package. urllib provides high-level interface for fetching data from given URLs.

httpget takes URLs as command line parameters and prints their contents in standard output (stdout). If the contents cannot be read before timeout, which is as short as one second, nothing is printed in stdout. In addition, fetching every URL creates a result that is printed in standard error. The result contains information about the successfulness and duration of the operation.

What is remarkable in httpget is that the behaviour of the standard library is altered to match what is wanted without touching its code. Here the timeout code is merged inside the library: our httpget program calls library function urlopen, which uses URLopener class like before.

It has come to my attention that the timeout example does not work in Jython (thanks to Xavier Farrero for pointing this out).

Conclusions and comparisons

aspects.py module implements one low-level functionality which helps to take a step towards AOP in Python. It does not provide a way to pack all aspect-related data in the same structure or any wildcards to define several join points in one pointcut like AspectJ does. But on the other hand, it does not introduce new syntax and it all can be done in a nice and dynamic Python way: while the program is running.

The Pythius project has produced utilities to assess the quality of Python code. The Pythius package also includes code for AOP in a much higher level than presented here.

However, there are some drawbacks in the Pythius's approach to AOP. For example, the classes to which aspects are applied must contain certain lines of code: the class defines exactly one aspect which is applied to it. In aspect oriented paradigm it would be more convenient to let the aspect define join points for its advices. This low-level implementation leaves this question open: you can define advices in one place and classes in another and bind them together in a third place. This makes modules that contain advices (such as tracer and timeout modules above) very reusable.

There are also other implementations:

Download

All files mentioned above are downloadable in aspects.tar.gz package. The code is released under the GNU GPL General Public License.


Antti Kervinen email: ask@cs.tut.fi
Last modified: Tue Feb 3 15:00:20 EET 2004
  翻译: