Trevor Hunsaker

Salt Lake area Web developer

Understanding Python decorators

A while back, I was hanging out on some Python forum and someone posted a comment about not being able to wrap their head around decorators.  I spent some time on what I thought was a helpful comment, and that was that. Recently I figured it'd be worthwhile to expand that comment a bit.  So, here we go.

First, it helpes to understand that python variables are not boxes in which you store values, but rather name tags that you hang onto objects.  Please don't move on until you grok this, it's the central principle to decorators.

Second, in Python, functions are objects. Function names themselves are just name tags hung onto their respective function objects.

Finally, here's a simple definition of a decorator: It's just a function.  Seriously. Nothing magic about it all. It takes another function as an argument (the function being decorated). It returns yet another function object.

If that sounded like I just said, "Function function function function function," (which is how I felt when trying to wrap my head around decorators), here's a small python program that may help out:

def orig_function():
    print "Hello!"

def my_decorator(function_to_decorate):  # Decorator takes a function as an argument
    def third_func():   # Yes, it's totally legit to create nested function definitions. 
    			# It's a very common decorator idiom.
        print "Take Off, eh?"

    return third_func # return a function object.  Note: we don't have parens here.

If we'd run this code in the interactive interpreter, here's the output we'd get:

>>> orig_function()
Hello!
>>> # now, we're going to decorate orig_function with my_decorator
>>> orig_function = my_decorator(orig_function)
>>> orig_function()
"Take off, eh?"

That above syntax is what made decorators click for me. All we've done here is taken the orig_function nametag, and hung it onto the third_function object (remember, my_decorator returns third_func). So when we call orig_function() now, what we're really calling is third_func().

I used the above syntax ( a=decorator(b) ) until I was sure I understood how decorators worked before I switched to using the shorter @ syntax.

orig_function = my_decorator(orig_function)

is equivalent to

@my_decorator
def orig_function():
    print "Hello!"

which isn't as intuitive and is probably the main roadblock to understanding decorators right at first, but nicer to use once you've got the hang of things.

In this example, we didn't do anything in the my_decorator function with the argument that was passed in ( orig_function ), but we could have called it and returned something other than third_func, depending on what orig_function returned. Remember, a decorator is just a function, you can do anything inside a decorator you'd do in a vanilla Python function. It just needs to take a function as a parameter, and return another function.