Briefly explaining Python3 Decorators

Decorators are a way to modify behavior of functions using other functions.

Take a look at the following routine:

1def sum(x, y):
2    return x + y

Say we wanted to alter this function so that it would always return a result by 1 larger than the sum. We can write a decorator for that:

1def increment(func):
2    def add_one(x, y):
3        return func(x, y) + 1
4
5    return add_one
6
7@increment
8def sum(x, y):
9    return x + y
1> sum(5, 4)
210

Under the hood, sum becomes increment(sum), i.e. sum = increment(sum). The function is modified, i.e. decorated by a new function returned by the decorator applied to it, which is why return values of decorators always have to be callable (i.e. - functions - though any object that implements the __call__ method is considered callabe).

That’s pretty much all there is to decorators.

This can be a little confusing, so to recap what is happening here:

  1. The reference to our original sum function is passed into its decorator, increment, and it is called
  2. From within it we construct a new inner function (add_one), which is returned, and our original function is overwritten by it
    • from within this function we call our original sum function - passing in any arguments (we have a reference to it!), adding one to its return value and returning it
    • this function has to take in the same number of arguments as the original function, because we intend to call our original function from within this new one

Another example

Knowing all this, we can, as an example, create a decorator that simply completely overwrites the result of any function it decorates.

1def overwrite(f):
2    return lambda: "OVERWRITTEN"
3
4@overwrite
5def some_complex_routine(*args):
6    # lots of complex logic here...
7    return "some complex result"
1> some_complex_routine()
2"OVERWRITTEN"

Here I use the lambda notation to create and return a simple function using a one-liner.

It is the equivalent of writing this:

1def overwrite(f):
2    def inner():
3        return "OVERWRITTEN"
4    return inner

Chaining multiple decorators

Decorating a single function using multiple decorators is possible, and the decorators are applied “from the bottom up” - i.e. the decorator “closest to” the function declaration is the one that gets applied first:

 1def plusone(func):
 2    def addone(x, y):
 3        return func(x, y) + 1
 4
 5    return addone
 6
 7def divtwo(func):
 8    return lambda a, b: func(a, b) / 2
 9
10@plusone
11@divtwo
12def sum(x, y):
13    return (x + y)
1> sum(5, 4)
25.5

By observing the result we can see that first 5 + 4 was divided by two, and then one was added to result.

Decorators with parameters

Unfortunately it isn’t as simple as making the decorator function accept additional arguments, and I was quite bummed to find that out.

Instead, a decorator with a parameter should actually be a function that takes an argument and returns a function which in turn returns another function. In other words, we have to call a function with a parameter that will then return a decorator function (built upon that paramater). You can think of it just like the decorators we’ve written above, but now they are returned from yet another function. Confusing (and I still don’t understand why, however there is probably a good exaplanation for it), so let’s rewrite the divtwo decorator so that it accepts the number to divide by as an argument:

 1def divby(n):
 2    def decorator(func): 
 3        def inner(a, b):
 4            return func(a, b) / n
 5        return inner
 6    return decorator
 7
 8@divby(3)
 9def sum(x, y):
10    return (x + y)
1> sum(5, 4)
23.0

Now the decorator has an argument which we can control on a per-function basis.

Since our decorator routine consists of simple one-liner statements we can rewrite it entirely using lambdas, however I personally think that, while more concise, it is less readable:

1def divby(n):
2    return lambda func: lambda a, b: func(a, b) / n

Decorating functions regardless of their arguments

The decorators I’ve written so far all assume that the decorated function accepts 2 arguments, and as such only works on those. We can fix this by using the familiar *args, **kwargs notation:

1def divby(n):
2    def decorator(func): 
3        def inner(*args, **kwargs):
4            return func(*args, **kwargs) / n
5        return inner
6    return decorator

Now this decorator can be applied to any function, regardless of the arguments it takes (or does not take!).