May 11, 2022

I’d never taken the time to wrap my head around decorators in Python, but I watched a video from PyCon 2021 today which explained them well. I decided to write this post to share the knowledge; read on for my explanation of decorators. In this post, I assume the reader is comfortable with functions in Python. If you’re not, you might want to do some preliminary reading.

You probably know that you can pass functions as arguments to other functions.
This is often used in e.g. the `map`

function.
For example, we can define a function `double`

that doubles its argument:

```
def double(x):
return x*2
```

If we check in the REPL, we see that `double`

is a function:

```
>>> double
<function double at 0x10638de50>
```

And we can pass it to `map`

and have it modify a list of numbers:

```
>>> list(map(double, [1,2,3]))
[2, 4, 6]
```

It’s good to keep this in mind as we get started with decorators.

Let’s say we want to create a decorator which ensures that a function’s result will always be even.
We will have some function `func`

, which will generate a random number, and we want to keep calling it until the result is even.

A higher-level function that would provide this guarantee for a `func`

could look like this:

```
def even_only(func, *args, **kwargs):
while True:
result = func(*args, **kwargs)
if result % 2 == 0:
return result
```

Nothing special to say here, if you know a bit of Python, this is pretty self-explanatory.

Now, to actually use this, we need a function to generate a random number.
That function could be anything, but for our purpose let’s just use a call to `random.randint`

:

```
import random
def get_number():
return random.randint(0, 101)
```

Now if we want to use this function in conjunction with `even_only`

, we call it as such:

```
>>> even_only(get_number)
30
```

The problem here is that we always need to include the call to `even_only`

, which means a lot of typing.
Also, if we forget to include a call to `even_only`

, we might get an odd number, and we *always* want an even number.
That is, the function `get_number`

*itself* is not guaranteed to return an even number.
So, let’s convert `even_only`

to a decorator and get that guarantee.

Here’s how we’d modify `even_only`

to work as a decorator:

```
def even_only(func):
def wrap(*args, **kwargs):
while True:
result = func(*args, **kwargs)
if result % 2 == 0:
return result
return wrap
```

The decorator `even_only`

“wraps” the original function.
The only new lines here are `def wrap(*args, **kwargs)`

, which defines the wrap function and allows any arguments to be passed into the function being wrapped (`get_number`

in this case), and `return wrap`

which returns the function.

Why does `even_only`

have to return the `wrap`

function (or in general, why does it need to return a function)?
Well, here’s how you’d use `even_only`

as a decorator for `get_number`

:

```
import random
@even_only
def get_number():
return random.randint(0, 101)
```

The `@`

syntax means that the definition of `get_number`

is replaced by the result of `even_only(get_number)`

.
It’s as if you wrote:

```
import random
def get_number():
return random.randint(0, 101)
get_number = even_only(get_number)
```

You then want to call `get_number()`

to get a random number; this is why `even_only`

has to return a function (in fact, *every* function decorator has to return a function).

Now if you call the decorated `get_number`

, you can be sure that it’ll always return an even number:

```
>>> for _ in range(200):
... assert(get_number() % 2 == 0)
...
>>>
```

Decorators can also take arguments.
Let’s say instead of even numbers, we want to ensure that the generator function will always return a number divisible by some `n`

.

Here’s how we’d create such a decorator, let’s call it `only_divisible_by`

:

```
def only_divisible_by(n):
def decorate(func):
def wrap(*args, **kwargs):
while True:
result = func(*args, **kwargs)
if result % n == 0:
return result
return wrap
return decorate
```

The internals of the function are the same as before, except we have to add one extra level of function definition, because `only_divisible_by`

is now a function itself, which accepts a single argument.
Let’s see how that looks in practice and clarify why you need that extra function.
This is how you’d use the decorator:

```
@only_divisible_by(3)
def get_number():
return random.randint(0, 101)
```

Let’s unroll the decorator syntax to see why we need three levels of functions:

```
def get_number():
return random.randint(0, 101)
get_number = (only_divisible_by(3))(get_number)
```

You see here that the first function call, `only_divisible_by(3)`

, must evaluate to a function itself, which we can interpret as an ‘instance’ of the inner `decorate`

function with `n`

set to 3.
We then call that instance of `decorate`

with the function `get_number`

as an argument, which must then return a function that we can call to generate numbers (`get_number()`

).
So in the end, `get_number`

is a call to the inner `wrap`

function with `n`

set to 3 and `func`

set to the original `get_number`

.

When using this decorated function, we know that the result will always be divisible by whatever `n`

we choose (3 in this case):

```
>>> for _ in range(200):
... assert(get_number() % 3 == 0)
...
>>>
```