Overview
Python decorators are one of my favorite Python features. They are the most user-friendly and developer-friendly implementation of aspect-oriented programming that I’ve seen in any programming language.
A decorator allows you to augment, modify or completely replace the logic of a function or method. This dry description doesn’t do decorators justice. Once you start using them you’ll discover a whole universe of neat applications that help keep your code tight and clean and move important “administrative” tasks out of the main flow of your code and into a decorator.
Before we jump into some cool examples, if you want to explore the origin of decorators a little more, then function decorators appeared first in Python 2.4. See PEP-0318 for an interesting discussion on the history, rationale and the choice of the name ‘decorator’. Class decorators appeared first in Python 3.0. See PEP-3129, which is pretty short and builds on top of all the concepts and ideas of function decorators.
Examples of Cool Decorators
There are so many examples that I’m hard pressed to choose. My goal here is to open your mind to the possibilities and introduce you to super-useful functionality you can add to your code immediately by literally annotating your functions with a one-liner.
The classic examples are the built-in @staticmethod and @classmethod decorators. These decorators turn a class method correspondingly to a static method (no self first argument is provided) or a class method (first argument is the class and not the instance).
The Classic Decorators
class A(object): @classmethod def foo(cls): print cls.__name__ @staticmethod def bar(): print 'I have no use for the instance or class' A.foo() A.bar()
Output:
A I have no use for the instance or class
Static and class methods are useful when you don’t have an instance in hand. They are used a lot, and it was really cumbersome to apply them without the decorator syntax.
Memoization
The @memoize decorator remembers the result of the first invocation of a function for a particular set of parameters and caches it. Subsequent invocations with the same parameters return the cached result.
This could be a huge performance booster for functions that do expensive processing (e.g. reaching out to a remote database or calling multiple REST APIs) and are called often with the same parameters.
@memoize def fetch_data(items): """Do some serious work here""" result = [fetch_item_data(i) for i in items] return result
Contract-Based Programming
How about a couple of decorators called @precondition and @postcondition to validate input argument as well as the result? Consider the following simple function:
def add_small ints(a, b): """Add two ints whose sum is still an int""" return a + b
If someone calls it with big integers or longs or even strings, it will quietly succeed, but it will violate the contract that the result must be an int. If someone calls it with mismatched data types, you’ll get a generic runtime error. You could add the following code to the function:
def add_small ints(a, b): """Add two ints in the whose sum is still an int""" assert(isinstance(a, int), 'a must be an int') assert(isinstance(a, int), 'b must be an int') result = a + b assert(isinstance(result, int), 'the arguments are too big. The sum is not an int') return result
Our nice one-line add_small_ints()
function just became a nasty quagmire with ugly asserts. In a real-world function it can be really difficult to see at a glance what it is actually doing. With decorators, the pre and post conditions can move out of the function body:
@precondition(isinstance(a, int), 'a must be an int') @precondition(isinstance(b, int), 'b must be an int') @postcondition(isinstance(retval, int), 'the arguments are too big. the sum is not an int') def add_small ints(a, b): """Add two ints in the whose sum is still an int""" return a + b
Authorization
Suppose you have a class that requires authorization via a secret for all its many methods. Being the consummate Python developer, you would probably opt for an @authorized method decorator as in:
class SuperSecret(object): @authorized def f_1(*args, secret): """ """ @authorized def f_2(*args, secret): """ """ . . . @authorized def f_100(*args, secret): """ """
That’s definitely a good approach, but it is a little annoying to repetitively do it, especially if you have many such classes.
More critically, if someone adds a new method and forgets to add the @authorized decoration, you have a security issue on your hands. Have no fear. Python 3 class decorators have got your back. The following syntax will allow you (with the proper class decorator definition) to automatically authorize every method of target classes:
@authorized class SuperSecret(object): def f_1(*args, secret): """ """ def f_2(*args, secret): """ """ . . . def f_100(*args, secret): """ """
All you have to do is decorate the class itself. Note that the decorator can be smart and ignore a special method like init() or can be configured to apply to a particular subset if needed. The sky (or your imagination) is the limit.
More Examples
If you want to pursue further examples, check out the PythonDecoratorLibrary.
What Is a Decorator?
Now that you’ve seen some examples in action, it’s time to unveil the magic. The formal definition is that a decorator is a callable that accepts a callable (the target) and returns a callable (the decorated) that accepts the same arguments as the original target.
Woah! that’s a lot of words piled on each other incomprehensibly. First, what’s a callable? A callable is just a Python object that has a call() method. Those are typically functions, methods and classes, but you can implement a call() method on one of your classes and then your class instances will become callables too. To check if a Python object is callable, you can use the callable() built-in function:
callable(len) True callable('123') False
Note that the callable() function was removed from Python 3.0 and brought back in Python 3.2, so if for some reason you use Python 3.0 or 3.1, you’ll have to check for the existence of the call attribute as in hasattr(len, '__call__')
.
When you take such a decorator and apply it using the @ syntax to some callable, the original callable is replaced with the callable returned from the decorator. This may be a little difficult to grasp, so let’s illustrate it by looking into the guts of some simple decorators.
Function Decorators
A function decorator is a decorator that is used to decorate a function or a method. Suppose we want to print the string “Yeah, it works!” every time a decorated function or method is called before actually invoking the original function. Here is a non-decorator way to achieve it. Here is the function foo() that prints “foo() here”:
def foo(): print 'foo() here' foo()
Output:
foo() here
Here is the ugly way to achieve the desired result:
original_foo = foo def decorated_foo(): print 'Yeah, it works!' original_foo() foo = decorated_foo foo()
Output:
Yeah, it works! foo() here
There are several problems with this approach:
- It’s a lot of work.
- You pollute the namespace with intermediate names like original_foo() and decorated_foo().
- You have to repeat it for every other function you want to decorate with the same capability.
A decorator that accomplishes the same result and is also reusable and composable looks like this:
def yeah_it_works(f): def decorated(*args, **kwargs): print 'Yeah, it works' return f(*args, **kwargs) return decorated
Note that yeah_it_works() is a function (hence callable) that accepts a callable f as an argument, and it returns a callable (the nested function decorated) that accepts any number and types of arguments.
Now we can apply it to any function:
@yeah_it_works def f1() print 'f1() here' @yeah_it_works def f2() print 'f3() here' @yeah_it_works def f3() print 'f3() here' f1() f2() f3()
Output:
Yeah, it works f1() here Yeah, it works f2() here Yeah, it works f3() here
How does it work? The original f1, f2 and f3 functions were replaced by the decorated nested function returned by yeah_it_works. For each individual function, the captured f callable is the original function (f1, f2 or f3), so the decorated function is different and does the right thing, which is print “Yeah, it works!” and then invoke the original function f.
Class Decorators
Class decorators operate at a higher level and decorate a whole class. Their effect takes place at class definition time. You can use them to add or remove methods of any decorated class or even to apply function decorators to a whole set of methods.
Suppose we want to keep track of all exceptions raised from a particular class in a class attribute. Let’s assume we already have a function decorator called track_exceptions_decorator that performs this functionality. Without a class decorator, you can manually apply it to every method or resort to metaclasses. For example:
class A(object): @track_exceptions_decorator def f1(): ... @track_exceptions_decorator def f2(): ... . . . @track_exceptions_decorator def f100(): ...
A class decorator that achieves the same result is:
def track_exception(cls): # Get all callable attributes of the class callable_attributes = {k:v for k, v in cls.__dict__.items() if callable(v)} # Decorate each callable attribute of to the input class for name, func in callable_attributes.items(): decorated = track_exceptions_decorator(func) setattr(cls, name, decorated) return cls @track_exceptions class A: def f1(self): print('1') def f2(self): print('2')
Conclusion
Python is well known for its flexibility. Decorators take it to the next level. You can package cross-cutting concerns in reusable decorators and apply them to functions, methods, and whole classes. I highly recommend that every serious Python developer get familiar with decorators and take full advantage of their benefits.
Comments