Python Decorators Tutorial from Coding compiler. Decorators are one of the most useful tools in Python, but for beginners they may seem confusing. Perhaps you have already met with them, for example, when working with Flask, but did not want to particularly delve into the essence of their work. This article will help you understand what decorators are and how they work.
What is a decorator?
Beginners decorators may seem inconvenient and incomprehensible, because they are beyond the scope of “ordinary” procedural programming like in C, where you declare functions that contain blocks of code and call them. The same applies to object-oriented programming, where you define classes and create objects based on them. Decorators do not belong to any of these paradigms and proceed from the field of functional programming. However, let’s not get ahead of ourselves, let’s look at everything in order.
A decorator is a function that allows you to wrap another function to extend its functionality without directly changing its code. That is why decorators can be considered as a practice of metaprogramming, when programs can work with other programs as with their data. To understand how this works, we first examine the functions in Python.
[Related Article: Regular Expressions For Web Developers]
How functions work
We all know what functions are, right? Don’t be so sure about that. Python functions have certain aspects that we rarely deal with, and, as a result, they are forgotten. Let’s clarify what functions are and how they are represented in Python.
Functions as a procedure
We are best acquainted with this aspect of functions. A procedure is a named sequence of computational steps. Any procedure can be called anywhere in the program, including within another procedure or even itself. There’s nothing more to say about this part, so let’s move on to the next aspect of functions in Python.
[Related Article: Programming Languages]
Functions as first class objects
In Python, everything is an object, not just objects that you create from classes. In this sense, it (Python) is fully consistent with the ideas of object-oriented programming. This means that in Python these are all objects:
- numbers;
- lines;
- classes (yes, even classes!);
- functions (what interests us).
The fact that everything is an object opens up many possibilities. We can save functions in variables, pass them as arguments and return from other functions. You can even define one function inside another. In other words, functions are first-class objects. From Wikipedia definition :
Objects of the first class in the context of a specific programming language are elements with which you can do everything the same as with any other object: pass as a parameter, return from a function, and assign to a variable.
And then functional programming comes into play, and decorators with it.
[Related Article: Most Popular UX Trends 2019 ]
Functional programming – higher order functions
Python uses some concepts from functional languages like Haskell and OCaml. Let’s skip the formal definition of a functional language and move on to its two characteristics inherent in Python:
- functions are first class objects;
- therefore, the language supports higher order functions.
Functional programming also has other properties like no side effects, but we are not here for that. We better concentrate on the other — higher order functions. What is a higher order function? Turn again to Wikipedia :
Functions of higher orders are those functions that can take as arguments and return other functions.
If you are familiar with the basics of higher mathematics, then you already know some of the mathematical functions of higher orders of order like the differential operator d / dx . It takes as input a function and returns another function derived from the original one. Higher-order functions in programming work in the same way — they either accept the function (s) at the input and / or return the function (s).
[Related Article: Amazon Machine Learning]
A couple of examples
Since we are familiar with all aspects of functions in Python, let’s demonstrate them in code:
def hello_world(): print('Hello world!')
Here we have defined a simple function. From the code snippet below, you will see that this function, like the classes with numbers, is an object in Python:
>>> def hello_world(): ... print('Hello world!') ... >>> type(hello_world) <class 'function'> >>> class Hello: ... pass ... >>> type(Hello) <class 'type'> >>> type(10) <class 'int'>
As you noticed, the function hello_world belongs to the type <class ‘function’>. This means that it is a class object function. In addition, the class we defined belongs to the class type. From all this, the head can spin, but after playing a little with the function, type you will understand everything.
[Related Article: Machine Learning vs. Deep Learning ]
Now let’s look at the functions as first-class objects.
We can store functions in variables:
>>> hello = hello_world >>> hello() Hello world!
Define functions inside other functions:
>>> def wrapper_function(): ... def hello_world(): ... print('Hello world!') ... hello_world() ... >>> wrapper_function() Hello world!
Pass functions as arguments and return them from other functions:
>>> def higher_order(func): ... print('The function received {} as an argument'.format(func)) ... func() ... return func ... >>> higher_order(hello_world) Function received <function hello_world at 0x032C7FA8> As an argument Hello world! <function hello_world at 0x032C7FA8>
From these examples it should be clear how flexible the functions in Python are. Given this, you can proceed to a discussion of decorators.
[Related Article: Artificial Intelligence And The Blockchain]
How do decorators work
A decorator is a function that allows you to wrap another function to extend its functionality without directly changing its code.
Since we know how higher-order functions work, now we can understand how decorators work. First, look at an example decorator:
def decorator_function(func): def wrapper(): print('Wrap function!') print('Wrapped function: {}'.format(func)) print('Perform wrapped function ...') func() print('We leave from the wrapper') return wrapper
Here decorator_function()is a function decorator. As you can see, it is a higher order function, since it takes a function as an argument, and also returns a function. Inside decorator_function()we have defined another function, a wrapper, so to speak, which wraps the argument function and then changes its behavior. The decorator returns this wrapper. Now look at the decorator in action:
>>> @decorator_function ... def hello_world(): ... print('Hello world!') ... >>> hello_world() Wrapped function: <function hello_world at 0x032B26A8> Perform wrapped function... Hello world! We leave from the wrapper
Magic, not otherwise! Just adding @decorator_functiona function before the definition hello_world(), we modified its behavior. However, as you might have guessed, the expression c @is just syntactic sugar for hello_world = decorator_function(hello_world).
In other words, the expression @decorator_functioncalls decorator_function()c hello_worldas an argument and assigns the name to the hello_worldreturned function.
[Related Article: Convolutional Neural Network ]
And although this decorator could cause a wow effect, it is not very useful. Let’s take a look at other, more useful (probably):
def benchmark(func): import time def wrapper(): start = time.time() func() end = time.time() print('[*] Время выполнения: {} секунд.'.format(end-start)) return wrapper @benchmark def fetch_webpage(): import requests webpage = requests.get('https://google.com') fetch_webpage()
Here we create a decorator, measuring the execution time of the function. Next, we use it on a function that makes a GET request to the Google homepage. To measure the speed, we first save time before executing the wrapped function, execute it, save the current time again and subtract the initial one from it.
After executing the code, we get something like this:
[*] lead time: 1.4475083351135254 секунд.
At this point, you probably began to realize how useful decorators can be. They extend the capabilities of the function without editing its code and are a flexible tool for changing anything.
[Related Article: Deep Learning Framework ]
Use arguments and return values.
In the examples above, the decorators did not accept or return anything. Modify our decorator to measure runtime:
def benchmark(func): import time def wrapper(*args, **kwargs): start = time.time() return_value = func(*args, **kwargs) end = time.time() print('[*] Время выполнения: {} секунд.'.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)
Output after execution:
[*] lead time: 1.4475083351135254 секунд. <!doctype html><html itemscope="" itemtype="https://schema.org/WebPage"........
As you can see, the arguments of the function being decorated are passed to the wrapper function, after which you can do anything with them. You can change the arguments and then pass them to the function being decorated, or you can leave them as they are or forget about them and pass on something completely different. The same applies to the value returned from the function being decorated, you can also do anything with it.
[Related Article: Artificial Intelligence And The Blockchain]
Decorators with Arguments
We can also create decorators that take arguments. Let’s look at an example:
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('[*] Среднее время выполнения: {} секунд.'.format(total/iters)) return return_value return wrapper return actual_decorator @benchmark(iters=10) def fetch_webpage(url): import requests webpage = requests.get(url) return webpage.text webpage = fetch_webpage('https://google.com') print(webpage)
Here we have modified our old decorator so that it performs the decorated function itersonce, and then displays the average execution time. However, to achieve this, I had to use the nature of the functions in Python.
[Related Article: Deep Learning Framework ]
benchmark()At first glance, a function may seem like a decorator, but in reality it is not. This is a normal function that takes an argument iters and then returns a decorator. In turn, it decorates the function fetch_webpage().
Therefore, we did not use an expression @benchmark, but @benchmark(iters=10)this means that a function is called here benchmark()(a function with brackets after it indicates a function call), after which it returns the decorator itself.
Yes, it can be really difficult to fit in your head, so keep the rule:
The decorator takes a function as an argument and returns a function.
In our example, it benchmark()does not satisfy this condition, since it does not take a function as an argument. While the function actual_decorator()that is returned benchmark()is the decorator.
[Related Article: Convolutional Neural Network ]
Decorators
Finally, it is worth mentioning that not only functions, but any callable objects can be a decorator. Instances of classes / objects with the method __call__()can also be called, so they can be used as decorators. This functionality can be used to create decorators storing a state. For example, here’s a decorator for memoization:
from collections import deque class Memoized: 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): 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_func def _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_value
Of course, this decorator is needed mainly for demonstration purposes, in a real application for such caching you should use it functools.lru_cache.
[Related Article: Apple Machine Learning Framework]
PS
Here will be listed some important things that were not covered in the article or were mentioned in passing. It may seem to you that they disagree with what was written in the article before that, but in fact it is not.
- Decorators do not have to be functions, it can be any callable object.
- Decorators are not required to return functions; they can return anything. But usually we want the decorator to return an object of the same type as the object being decorated.
[Related Article: SAP Business Intelligence Software]
Example:
>>> def decorator(func): ... return 'sumit' ... >>> @decorator ... def hello_world(): ... print('hello world') ... >>> hello_world 'sumit'
- Decorators can also take not only functions as arguments. Here you can read more about this.
- The need for decorators may not be obvious before writing the library. Therefore, if the decorators seem to you useless, look at them from the point of view of the library developer. A good example is the flask view designer.
- It is also worth paying attention to functools.wraps()– a function that helps to make the function being decorated look like the original one, doing such things as saving the doct string of the original function.
[Related Article: Blockchain Tutorial For Beginners]
Conclusion
We hope this article has helped you understand what kind of “magic” lies at the heart of the work of decorators.
Related Technical Articles:
Artificial Intelligence Trends
Learn Walt Disney’s Approach To Data Analysis
SIEM Tools List For Security Information Management
Robotic Process Automation Data Migration Case Study
RPA – The Line Between Reality and hype