Step-By-Step Introduction To Python Decorators

Python Decorator enables developers to modify or extend a function or class’s functionalities by wrapping it using a single line of code.
Python Decorators

Python decorators are extremely useful, albeit a bit hard to wrap your head around. They enable developers to modify or extend a function or class’s functionalities by wrapping it using a single line of code @decorator_name. Before we delve into decorators’ working, let’s understand the concept of first-class functions in Python. In a programming language, first-class objects are entities that have no restrictions on their usage. They can be dynamically created, stored in variables and data structures, passed as a function argument, or returned by a function. Everything in Python is a first-class object, even those “primitive types” in other languages.  Let’s see what this means for functions in Python:

  • Functions can be stored in variables and data structures.
 def square(x):
 def cube(x):
 def quartic(x):
 power_two = square

 powers = [square, cube, quartic]
  • They can be passed as an argument, returned by another function or be nested inside another function.
 def deocrator_function(func):
     '''A function that accepts another function '''
     def wrapper():
     return wrapper

 def f():
     '''Example function '''
     print("function f called")
 decorator = deocrator_function(f)
 decorator() #the inner wrapper function is returned so it needs to called 
 <function decorator_function.<locals>.wrapper at 0x7f73ecfa0dd0>
 function f called

Creating a Decorator

What we just did with the nested functions precisely what a decorator does but with a simpler syntax.  The function decorator_function is a decorator and can be used to wrap other functions using the @decorator_name syntax. 

 def g():
     ''' Yet another useless example function '''
     print("function g called")
 function g called

Using @decorator_function invokes the function decorator_function() with g as the argument and calls the returned wrapper() function. 

Subscribe to our Newsletter

Join our editors every weekday evening as they steer you through the most significant news of the day, introduce you to fresh perspectives, and provide unexpected moments of joy
Your newsletter subscriptions are subject to AIM Privacy Policy and Terms and Conditions.

Creating a Decorator for Functions with Arguments 

One issue with decorating functions with arguments is that the number of arguments can vary with the function. This can be easily overcome by using yet another immensely useful offering of Python: *args and **kwargs.

 def decorator_arguments(func):
     def wrapper(*args, **kwargs):
         func(*args, **kwargs)
     return wrapper

 def add(a, b, c, d):
     print("Sum is {}".format(a + b + c + d))
 add(10, 54, 13, 34) 
 Sum is 111

Decorators with Arguments

Sometimes the functionality introduced by the decorator will require additional arguments; this can be accommodated by nesting another function. The outermost function is responsible for the decorator argument, the inner functions for the function being decorated and the function arguments, respectively.

 def multiply(*outer_args, **outer_kwargs):
     def inner_function(func):
         def wrapper(*func_args, **func_kwargs):
             print(f"Times {outer_args[0]} is {outer_args[0] * func(*func_args, **func_kwargs)}")
         return wrapper
     return inner_function

 def basically_an_input(n):
     print(f"Input number {n}")
     return n
 Input number 5
 Times 99 is 495 

Decorating Classes

There are two ways of using decorators with classes; one can either decorate the individual methods inside the class or decorate the whole class.  Three of the most common Python decorators are used for decorating class methods, @property is used to create property attributes that can only be accessed through its getter, setter, and deleter methods.  @staticmethod and @classmethod are used to define class methods that are not connected to particular instances of the class. Static methods don’t require an argument, while class methods take the class as an argument. 

 class Account:
     def __init__(self, balance):
         self._balance = balance
     def balance(self):
         """Gets balance"""
         return self._balance
     def balance(self, value):
         """Set balance, raise error if negative"""
         if value >= 0:
             self._balance = value
             raise ValueError("balance must be positive")
     def new_account(cls):
         """Returns a new account with 100.00 balance"""
         return cls(100.00)
     def interest():
         """The interest rate"""
         return 5.25

 acc = Account(39825.75)
 acc.balance = 98621.75

 #testing if the setter is being used
     acc.balance = -354 
     print("Setter method is being used")
 acc2 = Account.new_account()
 print(f"Calling static method using class: {Account.interest()}, using instance {acc.interest()}") 
 Setter method is being used
 Calling static method using class: 5.25, using instance 5.25 

Now let’s see how one can decorate the whole class.

 import time
 def timer(example):
     def wrapper(*args, **kwargs):
         start = time.perf_counter()
         res = example(*args, **kwargs)
         end = time.perf_counter()
         run_time = end - start
         print("Finished in {} secs".format(run_time))
         return res
     return wrapper

 class Example:
     def __init__(self, n):
         self.n = n
         time.sleep(n if n < 3 else 2)
         print("Example running")
 x = Example(5) 
 Example running
 Finished in 2.0035361850023037 secs
 Note that decorating the class does not decorate all of its methods just __init__.   

Class as Decorator

Creating decorators as classes is useful in applications where the decorator might need to maintain a state. For making the class a decorator, it needs to be callable; this is achieved using the dunder method __call__. Furthermore, the __init__ method needs to take a function as an argument.

 class CountUpdates:
     def __init__(self, func):
         self.func = func
         self.version = 0
     def __call__(self, *args, **kwargs):
         self.version += 1
         print(f"Updating to version 0.3.{self.version}")
         return self.func(*args, **kwargs)

 def update():
     print("Update complete", end ="\n\n")
 Updating to version 0.3.1
 Update complete
 Updating to version 0.3.2
 Update complete
 Updating to version 0.3.3
 Update complete
 Updating to version 0.3.4
 Update complete

The Colab notebook for the above implementation can be found here

Aditya Singh
A machine learning enthusiast with a knack for finding patterns. In my free time, I like to delve into the world of non-fiction books and video essays.

Download our Mobile App


AI Hackathons, Coding & Learning

Host Hackathons & Recruit Great Data Talent!

AIM Research

Pioneering advanced AI market research

Request Customised Insights & Surveys for the AI Industry

The Gold Standard for Recognizing Excellence in Data Science and Tech Workplaces

With Best Firm Certification, you can effortlessly delve into the minds of your employees, unveil invaluable perspectives, and gain distinguished acclaim for fostering an exceptional company culture.

AIM Leaders Council

World’s Biggest Community Exclusively For Senior Executives In Data Science And Analytics.

3 Ways to Join our Community

Telegram group

Discover special offers, top stories, upcoming events, and more.

Discord Server

Stay Connected with a larger ecosystem of data science and ML Professionals

Subscribe to our Daily newsletter

Get our daily awesome stories & videos in your inbox

Subscribe to Our Newsletter

The Belamy, our weekly Newsletter is a rage. Just enter your email below.