Monday, November 13, 2006

Decorating Python

I thought I might throw together a quick example on what Python decorators are and how to use them, since I usually use them with CherryPy and I forgot exactly how they worked since I last used them.

Python decorators simply allow you to wrap a function call with one or more function calls. The idea is that you can write a decorator to modify (i.e., decorate) the behavior of many like functions. For example, in CherryPy I use a decorator on exposed class methods to wrap pages of content with an HTML header and footer.

For the purposes of this post, lets take a simple example.

def print_hello(name):
print "Hello, ", name

def print_goodbye(name):
print "Goodbye, ", name

print_hello("Matt")
print_goodbye("Matt")
This outputs:
Hello, Matt
Simple, right?

Okay, so maybe we want to gussy up our function, say by surrounding both print statements with a string of twenty dashes. We could modify each function like:
def print_goodbye2(name):
print "-" * 20
print "Goodbye, ", name
print "-" * 20

print_goodbye2("Matt")
Producing:
--------------------
Goodbye, Matt
--------------------
Of course to do this, we have to change both functions. What if we want to write the dash-output code and apply it to each function? This is where we can use a decorator!

In the following code, we create a decorator function called add_lines. It takes a function as an argument (the function its decorating), and returns a function _decor, which takes the arguments passed to the function it is decorating.
Inside the _decor closure, we print a dashed line, call the decorated function (with its expected arguments), and print another dashed line. Then, we define our functions print_hello and print_goodbye and we put @add_lines before both funtion definitions. This tells Python that we are using add_lines to decorate the following function.
def add_lines(fn):

def _decor(*args):
print "-" * 20
fn(*args)
print "-" * 20

return _decor
#end add_lines

@add_lines
def print_hello(name):
print "Hello, ", name

@add_lines
def print_goodbye(name):
print "Goodbye, ", name

print_hello("Matt")
print_goodbye("Matt")
The result of calling our decorated functions is:
--------------------
Hello, Matt
--------------------
--------------------
Goodbye, Matt
--------------------
So, when you call print_hello or print_goodbye you are effectively calling add_lines which in turn calls the original function.

You can also pass arguments to decorators using an additional closure:
def add_char_lines(char):     #function with expected arguments
def _wrap(fn): #closure that receives the decorated function
def _decor(*args): #closure that receives the decorated function's args
print char * 20
fn(*args)
print char * 20
return _decor
return _wrap
#end add_char_lines

@add_char_lines("*")
def print_hello(name):
print "Hello, ", name

print_hello("Matt")
Produces:
********************
Hello, Matt
********************
Finally, you can also apply multiple decorators to a function. The following works, even if it is an unlikely example:
def add_lines(fn):

def _decor(*args):
print "-" * 20
fn(*args)
print "-" * 20

return _decor
#end add_lines

def lower(fn):
def _decor(*args):
nargs = []
for arg in args:
if type(arg) == type(""):
nargs.append(arg.lower())
fn(*nargs)
return _decor
#end lower

@add_lines
@lower
def print_string(a_string):
print "STRING: ", a_string

print_string("A STRING")
Which produces:
--------------------
STRING: a string
--------------------
The Python wiki takes a much more in-depth look at decorators.

No comments: