Aspect-oriented Python
February 24th, 2009 at 10:09 pm (Python, Tutorial)
Tags: AOP, Python
A friend asked me for an example of AOP in Python. I started to write up my response, then realized it might be worth sharing more widely.
Briefly, AOP is about separating out Aspects which are are interspersed throughout your code (in AOP lingo, cross-cutting). The canonical example is logging, but there are others.
For Python, as long as you’re content with before/after aspects, the situation is good. In Python 2.5 and up there are two main tactics: decorators and context managers. Decorators use the @ syntax and context managers are used with the “with” keyword.
Decorators
Decorators are only usable on functions and methods in 2.4 and 2.5, but in 2.6, 3.0 and beyond they can be used on classes as well. Essentially they are callables (functions, methods, objects) that accept a function object and return a function object. They are called when the function is defined, so they get a chance to have their way with it: annotate it, replace it, or wrap it. The common case is to wrap it.
Quick example:
import logging
def before(fn):
def wrapped(*args, **kws):
logging.warn('about to call function %s' % fn.func_name)
return fn(*args, **kws)
return wrapped
def after(fn):
def wrapped(*args, **kws):
retVal = fn(*args, **kws)
logging.warn('just returned from function %s' % fn.func_name)
return retVal
return wrapped
OK, those are three basic wrappers, you can use them like so:
@before
def foo():
logging.warn('inside foo')
@after
def bar():
logging.warn('inside bar')
@before
@after
def baz():
logging.warn('inside baz')
foo()
bar()
baz()
This will result in the following output:
WARNING:root:about to call function foo
WARNING:root:inside foo
WARNING:root:inside bar
WARNING:root:just returned from function bar
WARNING:root:about to call function wrapped
WARNING:root:inside baz
WARNING:root:just returned from function baz
You will note that when we use two decorators on baz, the name of the function called by before
is “wrapped.” This is because what before
is called on is the result of after
. The functools.update_wrapper
function is useful in this case to make a wrapped function look more like the original function.
For more, please see PEP 318 Decorators for Functions and Methods: Examples and PEP 3129 Class Decorators. For convenience when creating new decorators, see the standard library functions functools.update_wrapper and functools.wraps.
Context Managers
Context Managers are used with the with
statement, and are handy for resource aquisition and release. In Python 2.5 you have to “from __future__ import with_statement” to use them, but they are built-in in Python versions later than that. Also, objects such as files and locks are context managers now, so you can use patterns like
with open('example.txt') as example:
for line in example:
do_something(line)
This will automatically close the file when leaving the with
block. And for locks the pattern is similar:
with myThreadingLock:
do_something_threadsafely()
It is important to note that the lock will be release properly, or the file closed, even if an exception is thrown inside the with
block.
If you want to create your own context managers, you can add two methods to your objects: __enter__(self)
and __exit__(self, exception_type, exception_value, traceback)
. The return value from __enter__
will be passed to the optional as
variable (seen in the file example). The __exit__
method will be called with exception info if there was an exception. If no exception is raised in the with
block, then all three arguements will be None
. If __exit__
returns True
then any exception will be “swallowed”, otherwise the exception will be re-raised after any cleanup.
For more info, see PEP 343 The “with” Statement, especially the examples section, and also the helper functions in the standard library contextlib.
AOP and 80/20
Full-on aspect-oriented programming is beyond the scope of this post and involves join-points, code weaving, and other such arcanery. There are multiple Python libraries which target aspect-oriented coding styles, but for my money, the simplicity of the methods in the standard library, coupled with my impression that they cover at least 80% of the uses of AOP, make me favour these built-in techniques over any of the special purpose tools.
Hacker’s Guide to Getting in Shape » Aspect-oriented Python and Metaclasses said,
February 25, 2009 at 9:25 am
[...] I came across a nice introductory post on AOP in Python by Dethe Elza titled Aspect-oriented Python. [...]
[Python] AoP python « Reading for Life said,
March 4, 2009 at 6:15 am
[...] Aspect-Oriented Python<http://livingcode.org/2009/aspect-oriented-python> [...]
gregturn said,
March 11, 2009 at 10:32 am
I agree that python makes it very easy to plugin things like AOP. In this situation, your AOP advice would be similar to AspectJ, where all instances of a class are wrapped with the advice. I coded an AOP module at http://springpython.webfactional.com/reference/html/aop.html that is based on a) not requiring advice to be applied to all instances and b) being non-intrusive and applicable even if you don’t have access to the original source. I think both of these are valid items any python developer should have.
delza said,
March 11, 2009 at 11:13 am
Hi Greg, thanks for the pointer. I intended this as a super-quick overview of the AOP-style features built in to recent versions of Python, so I didn’t explore aspects.py (http://www.cs.tut.fi/~ask/aspects/index.shtml), TransWarp (http://www.zope.org/Members/pje/Wikis/TransWarp/AOPTutorial/view) or its successor, PEAK (http://peak.telecommunity.com/Articles/WhatisPEAK.html). In part because these projects never really seemed to take off, and in part because my own preference is for the lighter, more explicit “AOP-lite” of decorators over the full-on code weaving and join points model. But I agree that both have their place in the toolbox.