Article

AOP and Python, part II

So the last article was written rather hastily during a bout of multi-tasking. I'd like to revisit the idea of aspect-oriented programming using Python's decorators.

There are a lot of good introductions to decorators, but I think Bruce Eckel's two-part series (Part One and Part Two) were the most useful for me when trying to understand how to create a decorator out of a class. And of course there is the ubiquitous Wikipedia article for those who want a fuller definition of AOP.

The basic idea behind AOP is that our code is often a stew of infrastructure and business logic. The canonical example is logging: every developer understands the importance of logging and well-instrumented code, even though we often don't expose it to the user. The most ubiquitous C debugger is, after all, printf.

What's a bit frustrating, though, is that logging code rarely has anything to do with the actual application we're writing, and it's tedious to read code that uses it. It's embarrassing to admit it, but there have been plenty of times in my career where I'd write code that looked like this:

if (<some condition>) {
    printf ("Break here: %s, %d\n", __FILE__, __LINE__);
}

Decorators don't exactly offer us line-by-line control the way that injecting printfs into the code does, but they do allow us to shift logic away from the business function by writing functions that return functions.

A classic example in Python is an entry-exit decorator that allows us to trace function execution:

def entex (func):
    def wrapped_func (*args, **kwargs):
        print ("Entering " + func.__name__)
        res = func (*args, **kwargs)
        print ("Exited " + func__name__)

        return res

    return wrapped_func

(There are a lot of times such a function could be helpful. In my previous work, we spent a lot of time working on syntax trees--at least of a sort--that were pretty complicated. Being able to trace snippets after the fact would have been pretty helpful, since stepping through a hundred functions was a bit of a pain. Post-mortem analysis was usually easier.)

Now then:

def hello_world (name):
    print ("Hello, " + name)

is about the simplest function you can write in Python. Let's instrument the entry and exit points:

@ent_ex
def hello_world (name):
    print ("Hello, " + name)

>>> hello_world("Ethan")
Entering hello_world
Hello, Ethan
Exited hello_world

The @ent_ex prefix is the Python decorator syntax, which is just a bit of sugar for hello_world = entex (hello_world). It's a boring decorator of course, but we can imagine there are other cases that could be more interesting. It's a bit unpythonic, but we could imagine imposing strict type checking:

@expects(str)
def hello_world (name):
    print ("Hello, " + name)

>>> hello_world(1)
<type 'int'> argument should be a <type 'str'>

This can be accomplished with a little wizardry:

def expects (*types):
    def wrap (func):
        def wrapped_func (*args, **kwargs):
            for arg, typ in zip(args, types):
              if type(arg) is not typ:
                 print (str(type(arg)) + " argument should be a " + str(typ))
                 return False

              else:
                 return func (*args, **kwargs)

        return wrapped_func

     return wrap

This is a little more complicated than the first example because the decorator function takes arguments. In this case, we need to have an enclosing scope to hold the arguments to the decorator prior to wrapping the function passed to it.

Naturally, decorators can be composed:

@entex
@expects(str)
def hello_world(name):
    print ("Hello, " + name)

This gets a little ugly, which any Pythonista will tell you:

Entering wrapped_func
Hello, Ethan
Exited wrapped_func

What we'd like, of course, is for entex to work on hello_world, but it instead applies to the expects function. There is a module, functools, that includes a decorator for just this purpose (wraps). Functools is super helpful and has a lot of utility (partial is really handy for IronPython event handling). Read the documentation for more, but here is a quick and dirty illustration:

from functools import wraps

def entex(func):
    @wraps(func)
    def wrapped_func (*args, **kwargs):
        print ("Entering " + func.__name__)
        res = func (*args, **kwargs)
        print ("Exited " + func.__name__)
        return res

    return wrapped_func

def expects (*types):
    def wrap (func):
        @wraps(func)
        def wrapped_func (*args, **kwargs):
            for arg, typ in zip(args, types):
                if type(arg) is not typ:
                    print (str(type(arg)) + " argument should be a " + str(typ))
                    return False

            res = func (*args, **kwargs)

            return res
        return wrapped_func

    return wrap

@entex
@expects(str)
def hello_world(name):
    print("Hello, " + name)

if __name__ == '__main__':
    hello_world ("Ethan")
    hello_world (1)

Entering hello_world
Hello, Ethan
Exited hello_world
Entering hello_world
<type 'int'> argument should be a <type 'str'>
Exited hello_world

As with any conversation about decorators, there's a lot left unsaid here. The idea of modifying function behavior is quite powerful: it allows us to extend the concept of decoupling components to decoupling functional logic. Code becomes a bit tidier and easier to read, which is never a bad thing, and it also frees developers to think more concisely about The Problem At Hand without being distracted by infrastructural concerns from the outset.