Python Decorators- A Weapon To Be Mastered

Amit Randive
Analytics Vidhya
Published in
5 min readMay 9, 2020

Hello Pythonist!. In this article we are gonna learn about Python Decorators. Instead directly jumping to its working I want you to know and feel the power of simple python functions and how they make Decorator such beautiful feature of Python.

Python functions possess three powers which is why they are known as “First Class Objects”.

What are those powers? What are they capable of? Let’s see -

  1. You can assign them to variables-
def parent():
print("Inside parent function")
fun=parentfun()>>
Inside parent function

Here “fun” variable holds the reference of parent while assignment parent function doesn’t get called. Now “fun” variable can be called as function which will execute parent function

2. They can be passed as values to other functions

def neighbor():
print("Hey, I am neighbor")
def parent(func):
print("hi there!")
func()
fun=parent
fun(neighbor)
>>
hi there!
Hey, I am neighbor

Here “neighbor” is passed as value to “parent” function and then called inside it.

3. They can be returned as values from other functions

def neighbor():
print("Hey, I am neighbor, where is your son?")
return 1
def parent(func):
print("hi there!")
call = func()
# nested function
def son():
print("Hi neighbor, I am his son")
# nested function
def daughter():
print("Hi neighbor, I am his daughter")

if call == 1:
return son
else:
return daughter
fun=parentchild = fun(neighbor) # returns reference of nested function>>
hi there!
Hey, I am neighbor, where is your son?

child() # nested function "son" gets called
>>
Hi neighbor, I am his son

Here we are defining two nested functions inside parent functions i.e. “son” and “daughter”. This nested functions neither be called directly from outside nor get executed automatically when outer function gets called. But we can return their references outside outer function and then they are able to get called from outside.

These three powers of python functions are ingredients for the most useful and powerful weapon i.e. Decorators.

Yes, It’s a weapon for Python Devs to shoot down many problems !!!

keep these 3 ingredients in mind and it will help you to understand decorators as we move forward.

What is Decorator?

Textbook definition- Decorators are functions which takes another callable (functions, methods and classes) and extends it behavior without explicitly modifying it.

In layman terms- Decorators wrap a callable, modifying its behavior.

Sound interesting?

Here it is an simple “Hello World” example of decorators -

def decorator(func):
def wrapper():
print("Before the function gets called.")
func()
print("After the function is executed.")
return wrapper
def wrap_me():
print("Hello Decorators!")
wrap_me = decorator(wrap_me)
wrap_me()
>>
Before the function gets called
Hello Decorators!
After the function is executed

How it works?

With above example we just saw all the three powers of python functions in action.

We are calling decorator by passing wrap_me function . When decorator gets called; wrapper function is defined holding reference of wrap_me (a closure) and uses it to wrap the input function (i.e. wrap_me) in order to modify its behavior at call time.

What is closure? -

Technique by which some data (in our case “wrap_me” function reference) gets attached to the code is called closure in Python.

This value in the enclosing scope is remembered even when the variable goes out of scope.

The wrapper closure has access to the undecorated input function and it is free to execute additional code before and after calling the input function.

decorator returns wrapper function’s reference which captured by wrap_me variable and when we call it we will be actually calling wrapper function.

At this moment the nested function gets executed and execute instructions before and after the original wrap_me function call.

It may feel bit heavy but once you fully understand how this work you will definitely fall in love with it❤

@ (Pi) Syntax

Instead of calling decorator on wrap_me and reassigning wrap_me variable you can use Python’s @ (pi) syntax.

def decorator(func):
def wrapper():
print("Before the function gets called.")
func()
print("After the function is executed.")
return wrapper
@decorator
def wrap_me():
print("Hello Decorators!")
wrap_me()>>
Before the function gets called
Hello Decorators!
After the function is executed

Using @ syntax is just adding syntactic sugar to achieve this commonly used pattern or shorthand for calling the decorator on an input function.

Decorators Which Accepts Arguments -

Now the obvious question anyone can ask- “How can I apply decorators to functions which takes arguments?”

Python allows to do so and it pretty simple as well. You remember we have something like *args and **kwargs to deal with variable number of arguments?

That’s it! This two guys right here ready for help.

Let’s write a decorator which logs function argument information-

def dump_args(func):
def wrapper(*args,**kwargs):
print(f'{args}, {kwargs}')
func(*args, **kwargs)
return wrapper
@dump_args
def wrap_me(arg1,arg2):
print(f"Arguments dumped")
wrap_me("arg_dump1","arg_dump2")>>
('arg_dump1', 'arg_dump2'), {}
Arguments dumped

It uses the * and ** operators in the wrapper definition to collect all positional and keyword arguments and stores them in variables args and kwargs respectively which is then forwarded to input function (i.e. wrap_me).

Decorators are reusable. That means you can use same decorator to multiple functions. Also you can import it from other modules as well.

Stacking Decorators

Multiple decorators can be used for single function.

E.g. Let’s say we need to log function’s execution time and also log its arguments-

import datetimedef dump_args(func):
def wrapper(*args,**kwargs):
print(f'{func.__name__} has arguments - {args}, {kwargs}')
func(*args, **kwargs)
return wrapper
def cal_time(func):
def wrapper(*args,**kwargs):
now = datetime.datetime.now()
print("start of execution : ", now.strftime("%d/%m/%Y %H:%M:%S"))
func(*args,**kwargs)
now = datetime.datetime.now()
print("end of execution : ", now.strftime("%d/%m/%Y %H:%M:%S"))
return wrapper
@cal_time
@dump_args
def wrap_me(arg1,arg2):
print("Arguments dumped")
wrap_me("arg_dump1","arg_dump2")

Here we have stacked two decorators on wrap_me function. Think what will be the behavior now?

This is what happened -

>>start of execution : 08/05/2020 21:13:11
wrap_me has arguments - ('arg_dump1', 'arg_dump2'), {}
Arguments dumped
end of execution : 08/05/2020 21:13:11

Decorators were applied from bottom to top. First, the input function was wrapped by the @dump_args decorator, and then the resulting (decorated) function got wrapped again by the @cal_time decorator.

Any generic functionality which you need to perform before actual callable execution, just simply write it as decorator and stick it on that particular callable.

E.g.

• logging

• enforcing authentication

• calculate performance of functions

  • caching etc.

As I said earlier that decorators are reusable building blocks that make this feature more powerful and it is frequently used in standard libraries as well as in third party libraries.

While writing Object Oriented Python code you often came across of @staticmethod, @abstractmethod, @classmethod right? Now you know what they actually mean. Try to explore what exactly this decorators do when we apply it to any function.

Just remember decorator should not be overused as its not solution to every problem but they are extremely useful if you know just when and where to use it.

If you reach here and understood everything in this article then Congratulations !! you just learned one of the most difficult concept in Python.

Cheers!!

--

--

Amit Randive
Analytics Vidhya

I like to know something about everything and everything about something| Write to express and share