So Easy, a Caveman Could Do It
A decorator is merely a function (or callable object) that accepts a function and returns a function. A very basic decorator can be defined like any other function:
def my_decorator(func):
return func
Functionally, the following two snippets are the same:
@my_decorator
def my_func():
pass
def my_func():
pass
my_func = my_decorator(my_func)
Notice that the function call is implied in the first case. The portion after the @
character is evaluated when the function is defined, and just has to resolve to a callable object. There is no magic involved here.
The Ol’ Bait an’ Switch
As you can see above, a decorator is a function that returns a function, but there’s no reason that it needs to return the function it was given, although it is polite to at least call that function at some point. Let’s try replacing the given function with one of our own:
def my_decorator(bait):
def switch():
print 'Mreow'
return switch
As you can see, this can be rather confusing for the caller of bark(). Let’s make a more useful decorator:
def trace(func):
def inner(*args, **kwargs):
print 'Entering function ' + func.__name__
return func(*args, **kwargs)
return inner
Let’s see how nice this is!
>>> @trace
... def bark():
... print 'Bark'
...
>>> bark()
Entering function: bark
Bark
Much nicer! Notice that our inner function takes any and all arguments and passes them into func. It doesn’t need to, but it does make it easier to pass the arguments into the original function. If you’re writing a decorator for a Django view, for example, you might use the signature inner(request, *args, **kwargs). Just don’t forget to pass request into func as well!
Shifty-Eyed Decorators
Decorators that take parameters tend to confuse people the most because it involves yet another level of nesting. It’s actually not complicated at all though. Remember, when you apply a decorator, you’re evaluating a statement that resolves to a callable. The trick to adding parameters is to create a function that you can call that returns the real decorator.
def trace(debug_level = 'WARNING'):
def decorator(func):
def inner(*args, **kwargs):
return func(*args, **kwargs)
return inner
return decorator
In this example, trace isn’t a decorator; it’s a decorator generating function.
Decorators Objectified
If you really don’t like using triply-nested functions to define your decorators, you can use a callable object instead. That is, your decorator takes parameters when you create the object and Python uses the call
method to decorate the function.
Here’s the trace decorator as a class:
class trace(object):
def __init__(self, debug_level = 'WARNING'):
self._debug_level = debug_level
def __call__(self, func):
self.func = func
return self.inner
def inner(self, *args, **kwargs):
return self.func(*args, **kwargs)
This object is used identically to the closure decorator above, but instead of calling a function that returns a function, you instead call the constructor to create a new callable object of type trace.
Personally, I dislike these because of the extra steps required to manually save the parameters and because I like my decorators to start with lowercase letters and my classes to start with uppercase letters.
Function Copycats
Sometimes, a function contains useful metadata that will be used elsewhere. For instance, if you add trace to a function that has already been decorated, trace will instead print out the name of the decorator instead of the function. We can get around that by using functools.update_wrapper to copy over the metadata. This can look a bit odd to write, but luckily for us, there’s a convenience decorator version, functools.wraps!
Here is the final version of trace:
from functools import wraps
def trace(debug_level = 'WARNING'):
def decorator(func):
@wraps(func)
def inner(*args, **kwargs):
return func(*args, **kwargs)
return inner
return decorator
Decorators with Class
Class decorators are decorators that you apply to a class instead of a function. You define them the same way that you define a function decorator, but you don’t usually replace them with a proxy class. In the case that you need to seriously modify a class, you may want to look into metaclasses, which allow you full access to the class before it is officially defined, including the __init__
function for the class instance, instead of just the object instance.
Written by Melissa Nuño. You should follow her on Github.