Deep Dive Into Python Decorators

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

Output:

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.

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:

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:

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:

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:

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:

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:

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”:

Output:

Here is the ugly way to achieve the desired result:

Output:

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:

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:

Output:

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:

A class decorator that achieves the same result is:

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.

Tags:

Comments

Related Articles