Demystifying @decorators in Python

Just by adding the @decorator_function statement before the def statement for the hello_world, we’ve modified it.

But as you may have already understood, the @ statement is just syntactic sugar for —hello_world = decorator_function(hello_world)In other words, all the @decorator_function statement doing is calling decorator_function with hello_world as its argument, and assigning the returned function to the name hello_world.

While this example may have been a “whoa” moment, the decorator wasn’t really a useful one.

Let’s look at some more examples, hopefully, useful ones.

def benchmark(func): import time def wrapper(): start = time.

time() func() end = time.

time() print('[*] Execution time: {} seconds.

'.

format(end-start)) return wrapper @benchmark def fetch_webpage(): import requests webpage = requests.

get('https://google.

com') fetch_webpage()Here I’ve taken created a decorator that would measure the time taken by a function to execute.

It’s a fairly useful decorator, I’ve used it on a function that GETs the homepage of Google.

As you can see, I’ve saved the time before calling the wrapped function, and after calling the wrapped function, and by subtracting those two I got the time of execution.

On running the above I got the following output[*] Execution time: 1.

4326345920562744 seconds.

You must have started realizing how useful decorators can be.

It adds functionality to a function without modifying the original code, and it gives you complete flexibility over what you want to do // modify.

The possibilities are endless!Using arguments and return-valueIn the examples we’ve looked at so far, the decorated functions were neither taking any arguments, nor they were returning anything.

Let’s look expand the benchmark decorator to include that.

def benchmark(func): import time def wrapper(*args, **kwargs): start = time.

time() return_value = func(*args, **kwargs) end = time.

time() print('[*] Execution time: {} seconds.

'.

format(end-start)) return return_value return wrapper @benchmark def fetch_webpage(url): import requests webpage = requests.

get(url) return webpage.

text webpage = fetch_webpage('https://google.

com') print(webpage)Of which the output is[*] Execution time: 1.

4475083351135254 seconds.

<!doctype html><html itemscope="" itemtype="http://schema.

org/WebPage".

.

You can see that the arguments of the decorated function get passed to the wrapper function, and then you’re free to do anything with it.

You can modify the arguments and then pass them to the decorated function, or you can pass them unmodified, or you can discard them completely and pass whatever pleases you to the decorated function.

Same goes with the returned value from the decorated function, do whatever you want with it.

Here in the example, I’ve kept the arguments and the return value of the decorated function unmodified.

Decorators with argumentsWe can also define decorators which take arguments.

It’d be best to look at the code in order to understand this —def benchmark(iters): def actual_decorator(func): import time def wrapper(*args, **kwargs): total = 0 for i in range(iters): start = time.

time() return_value = func(*args, **kwargs) end = time.

time() total = total + (end-start) print('[*] Average execution time: {} seconds.

'.

format(total/iters)) return return_valuereturn wrapper return actual_decorator@benchmark(iters=10)def fetch_webpage(url): import requests webpage = requests.

get(url) return webpage.

textwebpage = fetch_webpage('https://google.

com')print(webpage)Here I’ve extended the benchmark decorator so that it runs the decorated function a given number of times (specified using the iters parameter), and then prints out the average time taken by the function to execute.

But to do this, I’ve had to use a little trick — exploiting the nature of functions in Python.

The benchmark function, which may at first look like a decorator, is not really a decorator.

It’s a normal function, which accepts the argument iters, and returns a decorator.

That decorator, in turn, decorates the fetch_webpage function.

That’s why we haven’t used the statement @benchmark, rather we’ve used @benchmark(iters=10) — meaning the benchmark function is getting called here (a function with parentheses after it signifies a function call) and the return value of that function is the actual decorator.

It’s admittedly a kind of tricky to understand, the following rule-of-thumb will help youDecorator function takes a function as an argument, and returns a function.

Here in the example, the benchmark function doesn’t satisfy this rule of thumb, because it doesn’t take a function as an argument.

Whereas the actual_decorator function—which is returned by benchmark—is a decorator, because it satisfies the above rule-of-thumb.

Objects as decoratorsLastly, I want to mention that not only functions but any callable can also be a decorator.

Class instances // objects with a __call__ method can be called too, so that can be used as a decorator as well.

This functionality can be used to create decorators with some kind of “state”.

For example, Scott Lobdell shows how he created a memoization decorator in his blog here.

I’ll post the code here but won’t go into detail explaining it, you can check out his blog post for a write-up on this.

from collections import dequeclass Memoized(object): def __init__(self, cache_size=100): self.

cache_size = cache_size self.

call_args_queue = deque() self.

call_args_to_result = {}def __call__(self, fn, *args, **kwargs): def new_func(*args, **kwargs): memoization_key = self.

_convert_call_arguments_to_hash(args, kwargs) if memoization_key not in self.

call_args_to_result: result = fn(*args, **kwargs) self.

_update_cache_key_with_value(memoization_key, result) self.

_evict_cache_if_necessary() return self.

call_args_to_result[memoization_key] return new_funcdef _update_cache_key_with_value(self, key, value): self.

call_args_to_result[key] = value self.

call_args_queue.

append(key)def _evict_cache_if_necessary(self): if len(self.

call_args_queue) > self.

cache_size: oldest_key = self.

call_args_queue.

popleft() del self.

call_args_to_result[oldest_key]@staticmethod def _convert_call_arguments_to_hash(args, kwargs): return hash(str(args) + str(kwargs))@Memoized(cache_size=5)def get_not_so_random_number_with_max(max_value): import random return random.

random() * max_valueThis example is mostly for demonstration purposes though, in a real application you should use functools.

lru_cache to implement caching functionality like this.

ConclusionI hope this post helped you understand the power of decorators, and also the “magic” behind it.

If you have any questions, post it in the comments down below and I’ll try my best to get back to you.

P.

S.

Some things I want to say here that are important but I left out in the article or didn’t make it clear enough.

These may seem to go against what I said in the entire article, but actually they don’t.

I just didn’t mention them at the start so that the reader doesn’t get overwhelmed.

Decorators need not be functions, it can be any callable.

Decorators need not return functions, they can return anything.

But usually we want decorators to return the objects of the same type as of the decorated object.

Thanks to /u/zardeh and /u/mafrasi2 at Reddit for pointing this out.

>>> def decorator(func):.

return 'sumit'.

>>> @decorator.

def hello_world():.

print('hello world').

>>> hello_world'sumit'Decorators also need not take only functions as input.

Thanks to /u/hchasestevens at Reddit for pointing this out, check this article out to learn more about it.

I’ve found that the need for decorators only becomes clear when you write a library.

So if decorators still seem kind of useless to you, think of it from the POV of a library developer.

A good example would be the view decorator in Flask.

When I said that decorators don’t modify functions permanently, what I meant is that it can be easily removed and added with just one line.

You should look into functools.

wraps — it’s a helper function that helps you make a decorated function look like the original function, doing things such as keeping the docstring of the original function.

Thanks to /u/primordial_pouch and /u/Overload175 over at Reddit for pointing this out.

.

. More details

Leave a Reply