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:
- The reference to our original
sum
function is passed into its decorator,increment
, and it is called - 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
- from within this function we call our original
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!).