Decorators in Python
For a beginner reading this guide, it is important to read on Scope and Closure before moving on to decorator.
Short recap on scope and closure
-A function(inner function) inside another function(outer function) can access all variables in the outer function(Enclosing Scope)
-A function can return another function(i.e. in Closure)
Going forward, it is important to clarify what is meant by function being a first class citizen in Python. This simply means that they can be passed around just like how you treat variables having type as integer, string, float, etc.
Let’s look at these examples:
def function_a():
print('Funtion A works')
b = function_a
b()
[*]Output
Funtion A works
Here we defined a function called ‘function_a’ which we later assigned it to a variable ‘b’(Note that we didn’t call the function while assigning to b).
We are able to treat function_a like other objects(int, str, etc.) by assignment. We later use variable ‘b’ to call the function
-Passing a function as argument.
Since we have successfully been able to call a function after it’s has been assigned to a variable (i.e. b)in the example given above, passing it to a function as argument comes as no surprise.
[*]snippet
from typing import Callable
def function_ a():
print("Print from Function A")
def function_b(func: Callable):
func()
print("Print from Function B, the caller")
function_b(function_a) #calling function_b
[*]Output
Print from Function A
Print from Function B, the caller
We defined function_b and it’s expected to take a function as a parameter
-So far we have been able to solidify the needed prerequisite to understand decorator.
Decorator is a function which takes another function as an argument with the sole aim of extending/decorating it without explicitly modifying the original function.
You can think of it as a wrapper which give additional functionality to an already existing function.
Say we have a function ‘greet’ that calls only a name out when greeting(maybe it does not know how to pronounce “Hi”). We can decorate it by adding a greeting message to it.
def greet():
'''A function that calls a name '''
return "Ray"
def greeting_decorator(func):
def wrapper():
name = func()
decorated_name = "Hi, " + name
return decorated_name
return wrapper
decorate = greeting_decorator(greet)
print(decorate())
The greeting_decorator is a function which takes in another function(Callable) as an argument. Inside it, there is a wrapper function that does the whole decoration. The wrapper which is an inner function is able to access variables(i.e. func) in the outer function(i.e. greeting_decorator) due to Enclosing Scope. The wrapper function does all that is needed to add to extra flavour/behaviour to the initial function ‘func’. Finally the wrapper function is returned(similar to closure)
decorate = greeting_decorator(greet)
calling greeting_decorator(greet) would only return the inner function(wrapper) object which we assigned to ‘decorate’ variable.
decorate() calls the inner function(wrapper) which finally returns what we wanted it to return(decorated name)
[*]Output
Hi, Ray
Based on the observation in code above, we can say that decorator wraps another function and extra functionality to it.
The above syntax works but Python syntax(@) to decorate a funtion. We simply place @name_of_decorator on top of the function we want to decorate.
[*]Syntax
@greeting_decorator
def greet():
return "Ray"
print(greet())
We are simple saying we want to decorate function ‘greet’ with ‘greeting_decorator’.
[*]Output
Hi, Ray
Another question we need to answer is how can we pass arguments from a function to the decorator for use?
The previous example uses a hard-coded string ‘Ray’. What if we need it to greet someone else?
We can simply list the arguments needed in the wrapper and use it
[*]Snippet
def greeting_decorator(func):
def wrapper(arg1):
name = func(arg1)
decorated_name = "Hi, " + name
return decorated_name
return wrapper
@greeting_decorator
def greet(name: str):
return name
print(greet("Kay"))
[*]Output
Hi, Kay
Though the @syntax is widely used. Let’s pass the argument when are are not using @.
def greet(name: str):
'''A function that calls your name out as a greeting '''
return name
decorate = greeting_decorator(greet)
print(decorate("KAY"))
[*]OUTPUT
Hi, KAY
This even makes it more clearer that in the decorator, the inner function(wrapper) is returned(just like Closure) and it is the function we are always calling.
Sometimes we may not know ahead the number of arguments that would be passed to the function we want to decorate.
Using *args allow us to gather positional arguments(args) as a tuple while **kwargs will collect keyword arguments(kwargs) as dictionary. arg and kwargs are just the convention in the python community.
Let’s modify the greet function:
def greeting_decorator(func):
def wrapper(*args,**kwargs):
print("Postional Args",args)
print("Kwargs Args",kwargs)
name = func(args[0], args[1])
decorated_name = "Hi, " + name[0]
return decorated_name
return wrapper
@greeting_decorator
def greet(name,age):
return name,age
print(greet("Kay",20))
[*]Output
Postional Args (‘Kay’, 20)
Kwargs Args {}
Hi, Kay
Decorating a funcion with multiple decorators(Chaining Decorators).
It is possible to decorate a function with more than one decorator. The order of execution is base->top. That means decorator1 will execute before decorator2 as show below
@decorato2
@decorator1
def ordinary_function():
pass
Let’s add another decorator that add prefix to a name i.e Mr,Mrs, etc.
[*]Code Snippet
def prefix_decorator(func):
def wrapper(arg):
name = func(arg)
prefixed_name = f'Mr. {name}'
return prefixed_name
return wrapper
def greeting_decorator(func):
def wrapper(arg1):
name = func(arg1)
decorated_name = "Hi, " + name
return decorated_name
return wrapper
Now we have two different decorators, one for adding a welcoming greeting while the other adds prefix to the name of the user
[*]Usage
@greeting_decorator
@prefix_decorator
def greet(name: str):
'''A function that calls your name out as a greeting '''
return name
print(greet("Kay"))
[*]Output
Hi, Mr. Kay
Note the other of the decorators, the execution is always from the base to the top. So we added the prefix(i.e Mr) to the name before adding ‘Hi’
Let’s reverse the order of the generators:
[*]Usage
@prefix_decorator
@greeting_decorator
def greet(name: str):
'''A function that calls your name out as a greeting '''
return name
print(greet("Kay"))
[*]Output
Mr. Hi, Kay
As we have seen from all the examples above, decorator wraps function.
Let’s try to access the name and doc of the function we are decorating(i.e. greet)
[*]Code
def prefix_decorator(func):
def wrapper(arg):
'''Wrapper doc'''
name = func(arg)
prefixed_name = f'Mr. {name}'
return prefixed_name
return wrapper
@prefix_decorator
def greet(name: str):
'''A function that only call your name'''
return name
print(greet.__name__)
print(greet.__doc__)
[*]Output
wrapper
Wrapper doc
It’s obvious that the decorator hides the the greet function metadata so it only returns the metadata of the closure(wrapper function) inside it.
The solution to this is to use functools provided by python
[*]Code
import functools
def prefix_decorator(func):
@functools.wraps(func)
def wrapper(arg):
'''Wrapper doc'''
name = func(arg)
prefixed_name = f'Mr. {name}'
return prefixed_name
return wrapper
@prefix_decorator
def greet(name: str):
'''A function that only call your name'''
return name
print(greet.__name__)
print(greet.__doc__)
[*]Output
greet
A function that only call your name
Some uses of decorator
- Authentication and Permission check in an application(i.e. a decorator to check if user is authenticated widely used in Django/Flask)
- Measuring execution time of a program
Thanks for reading and feel free to explore my video collection on YouTube for more educational content. Don’t forget to subscribe to the channel to stay updated with future releases.