A brief explanation of Python Decorators
This article was published as a part of the Data Science Blogathon
Introduction
Decorators are simply callables for decorating a function. It helps add new functionalities to a function without changing its original structure. In this article, we are going to learn the hows, whats, and whys of decorators. But before delving into Decorators we must get familiarized with certain concepts like first-class citizens, nesting of functions, closures, nonlocal scopes, etc. These topics are essential for understanding Python Decorators. We will go through each topic one by one to ensure complete clarity.
Table of contents
- First-class citizens
- Assigning functions to variables
- Functions as parameters
- Functions returning other functions
- Nesting of functions
- Closures
- Decorators
- Passing arguments to the decorator
- Generalizing decorators for multiple parameters
- Applying multiple decorators to a function
- functools.wraps()
- EndNote
First-class citizens in Python
In Python, any object that can be assigned to a variable, passed as an argument, returned from a function will be considered as a first-class citizen. in short, there are almost no restrictions on their uses. Some of the examples are data types like int, floats, strings, etc, data structures like lists, tuples, etc. Python functions also satisfy the requirements for being a first-class citizen. This is a fundamental concept to understand the creation of Decorators.
Assigning functions to variables
Like any other object like lists, tuples, or dictionaries Python functions can also be assigned to variables. For example:
def greet(msg): return f'hello! {msg}' var = greet #function greet is assigned to var var('Peter') # var is called
output: 'hello! Peter'
Functions as parameters
def upper_text(msg): return msg.upper() def greet(func): var = func('hello! Peter') return var greet(upper_text) #func was sent as a parameter to my_func
Output: 'HELLO! PETER'
Functions returning other functions
def outer(): def inner(): return 'Freedom in thought' return inner var = outer() print(var())
output: 'Freedom in thought'
Nesting Of Functions
def outer(x): print(f'Hey! {x} this is outer function') def inner(): print(f'Hey! {X} this is inner function') inner() outer('Jose')
output: Hey! Jose this is outer function HEy! Jose this is inner function
Hence, we learned that the nested functions are also able to access the objects that are present in the enclosing scope(outer() in this case). But the opposite isn’t true, Objects in the inner() scope can not be accessed by outer().
Nonlocal Scope in Python
The nonlocal scope comes into the picture when we deal with nested functions. The scope of nested functions is called nonlocal scopes and variables defined inside of them are called nonlocal variables. These variables can neither be in local scope nor in global. Let’s understand this by an example.
def outer(x): def inner(): x = 'Tom' print(f"{x}'s spider-man is the best") inner() print(f"{x}'s spider-man is the best") outer('Tobey')
output: Tom's spider-man is the best Tobey's spider-man is the best
def outer(x): def inner(): nonlocal x x = 'Tom' print(f"{x}'s spider-man is the best") inner() print(f"{x}'s spider-man is the best") outer('tobey')
output: Tom's spider-man is the best Tom's spider-man is the best
Note: Changing the value of a nonlocal variable will also be reflected in the local scope.
In the above example change in the value of a variable in nonlocal scope also changed the variable in the local scope.
Closures in Python
def outer(text): "enclosing function" def inner(): "nested function" print(text) return inner var = outer('food for brain') var()
output: food for brain
The technique by which some data is attached to some code even after the execution of the original function is finished is called closures. Even if we delete the original function the values in the enclosing scope are remembered.
Decorators in Python
def outer(f): def inner(): msg = f() return msg.upper() return inner def func(): return 'hello! Peter' func = outer(func) print(func())
output: HELLO! PETER
However, Python has a better way to implement this, we will use @ symbol before the decorator function. This is nothing but syntactic sugar. Let’s see how it’s done
def outer(f): def inner(): msg = f() return msg.upper() return inner @outer def func(): return 'hello! Peter' func()
output: HELLO! PETER
def outer(func): def inner(args): return [func(var[0],var[1]) for var in args] return inner @outer def func(a,b): return a if a>b else b print(func([(1,4),(5,3)]))
output: [4, 5]
Passing arguments to decorators in Python
We can also pass arguments to the decorators themselves, see the following example
def meta_decorator(x): def outer(func): def inner(args): return [func(var[0],var[1])**x for var in args] return inner return outer @meta_decorator(2) def func(a,b): return a if a>b else b print(func([(1,4),(5,3),(6,5)]))
output: [16, 25, 36]
Generalizing decorators for multiple parameters in Python
Just as any other function we can take the help of *args and **kwargs to generalize decorators for multiple parameters intake. *args will be a tuple of positional arguments and **kwargs will be a dictionary for keyword arguments. Let’s see an example.
def outer(func): def inner(*args,**kwargs): func() print(f'poistional arguments {args}') print(f'keyword argumenrs are {kwargs}') return inner @outer def func(): print('arguments passed are shown below') func(6,8,name='sunil',age=21)
output: arguments passed are shown below poistional arguments (6, 8) keyword argumenrs are {'name': 'sunil', 'age': 21}
From the above example, we learnt how to pass multiple parameters to the decorator function. Here we passed both positional arguments as well as keyword arguments in a single line. Remember the convention is to use positional arguments before keyword arguments.
Original functions or those that are to be decorated can also take arguments, but those arguments need to be passed to the function from the wrapper or inner function.
def outer(func): def inner(*args,**kwargs): func(2) #arguments passed to the original function print(f'poistional arguments {args}') print(f'keyword argumenrs are {kwargs}') return inner @outer def func(a): print(f'arguments for {a} cases are shown below') func(6,9,name='sunil',age=21)
output: arguments for 2 cases are shown below poistional arguments (6, 9) keyword argumenrs are {'name': 'sunil', 'age': 21}
Applying Multiple decorators in Python
def square(func): def inner_one(): prime_nums = func() return [i**2 for i in odd_nums] return inner_one def find_prime(func): def inner_two(): prime = [] for i in func(): count = 0 for j in range(1,i): if i%j==0: count+=1 if count<2: prime.append(i) return prime return inner_two @square @find_prime def printer(): return [5,8,4,3,11,13,12] printer()
output: [25,9,121,169]
Here, find_prime() was executed first and then square(). If we change the order the result will be an empty list(why?).
The functools.wraps()
So far so good but there is a problem that we have overlooked. See the below example
def outer(func): def inner(): 'inside inner function' msg = func() return msg.upper() return inner @outer def function(): 'inside original function' return 'hello! Peter' #if we run this we gwt print(function.__name__) print(function.__doc__)
In the above example, we saw function.__name__ showed inner while it should have been ‘function’ and same for docstrings too. The function() got replaced by inner(). It overrode the name and docstring of the original function, but we want to retain the information of our original function. So to do that Python provides a simple solution i.e. functools.wraps().
from functools import wraps def outer(func): @wraps(func) def inner(): 'inside inner function' msg = func() return msg.upper() return inner @outer def function(): 'inside original function' return 'hello! Peter' #if we run this we gwt print(function.__name__) print(function.__doc__)
output:function inside original function
In the above example, we used the wraps() method of functions inside the outer(). Observe that the wraps() method here itself was used as a decorator with func() as the argument. This decorator stores the metadata(name, docstring, etc) of the function to be decorated. Not doing this will not be harmful but will make debugging tedious, So it is prudent to use functools.wraps() whenever decorators are used.