Open In App

Decorators in Python

Last Updated : 07 Jan, 2025
Comments
Improve
Suggest changes
Like Article
Like
Report

In Python, decorators are a powerful and flexible way to modify or extend the behavior of functions or methods, without changing their actual code. A decorator is essentially a function that takes another function as an argument and returns a new function with enhanced functionality.

Decorators are often used in scenarios such as logging, authentication and memorization, allowing us to add additional functionality to existing functions or methods in a clean, reusable way.

Decorator Example:

Python
# A simple decorator function
def decorator(func):
  
    def wrapper():
        print("Before calling the function.")
        func()
        print("After calling the function.")
    return wrapper

# Applying the decorator to a function
@decorator

def greet():
    print("Hello, World!")

greet()

Output
Before calling the function.
Hello, World!
After calling the function.

Explanation:

  • decorator takes the greet function as an argument.
  • It returns a new function (wrapper) that first prints a message, calls greet() and then prints another message.
  • The @decorator syntax is a shorthand for greet = decorator(greet).

Let's explore decorators in detail:

Syntax of Decorator Parameters

def decorator_name(func):
def wrapper(*args, **kwargs):
# Add functionality before the original function call
result = func(*args, **kwargs)
# Add functionality after the original function call
return result
return wrapper

@decorator_name
def function_to_decorate():
# Original function code
pass

Explanation of Parameters

1. decorator_name(func):

  • decorator_name: This is the name of the decorator function.
  • func: This parameter represents the function being decorated. When you use a decorator, the decorated function is passed to this parameter.

2. wrapper(*args, **kwargs):

  • wrapper: This is a nested function inside the decorator. It wraps the original function, adding additional functionality.
  • *args: This collects any positional arguments passed to the decorated function into a tuple.
  • **kwargs: This collects any keyword arguments passed to the decorated function into a dictionary.
  • The wrapper function allows the decorator to handle functions with any number and types of arguments.

3. @decorator_name:

  • This syntax applies the decorator to the function_to_decorate function. It is equivalent to writing function_to_decorate = decorator_name(function_to_decorate).

Higher-Order Functions

In Python, higher-order functions are functions that take one or more functions as arguments, return a function as a result or do both. Essentially, a higher-order function is a function that operates on other functions. This is a powerful concept in functional programming and is a key component in understanding how decorators work.

Key Properties of Higher-Order Functions:

  • Taking functions as arguments: A higher-order function can accept other functions as parameters.
  • Returning functions: A higher-order function can return a new function that can be called later.

Example of a Higher-Order Function:

Python
# A higher-order function that takes another function as an argument
def fun(f, x):
    return f(x)

# A simple function to pass
def square(x):
    return x * x

# Using apply_function to apply the square function
res = fun(square, 5)
print(res)  

Output
25

In this example, first function fun is a higher-order function because it takes another function f as an argument and applies it to the value x.

Role in Decorators:

Decorators in Python are a type of higher-order function because they take a function as input, modify it, and return a new function that extends or changes its behavior. Understanding higher-order functions is essential for working with decorators since decorators are essentially functions that return other functions.

Functions as First-Class Objects

In Python, functions are first-class objects, meaning that they can be treated like any other object, such as integers, strings, or lists. This gives functions a unique level of flexibility and allows them to be passed around and manipulated in ways that are not possible in many other programming languages.

What Does It Mean for Functions to Be First-Class Objects?

  • Can be assigned to variables: Functions can be assigned to variables and used just like any other value.
  • Can be passed as arguments: Functions can be passed as arguments to other functions.
  • Can be returned from other functions: Functions can return other functions, which is a key concept in decorators.
  • Can be stored in data structures: Functions can be stored in lists, dictionaries, or other data structures.
Python
# Assigning a function to a variable
def greet(n):
    return f"Hello, {n}!"

say_hi = greet  # Assign the greet function to say_hi
print(say_hi("Alice"))  # Output: Hello, Alice!

# Passing a function as an argument
def apply(f, v):
    return f(v)

res = apply(say_hi, "Bob")
print(res)  # Output: Hello, Bob!

# Returning a function from another function
def make_mult(f):
    def mult(x):
        return x * f
    return mult

dbl = make_mult(2)
print(dbl(5))  # Output: 10

Output
Hello, Alice!
Hello, Bob!
10

Explanation:

  • The code defines a greet function that returns a greeting message.
  • The greet function is assigned to the say_hi variable, which is used to print a greeting for "Alice".
  • Another function, apply, takes a function and a value as arguments, applies the function to the value, and returns the result.
  • apply is demonstrated by passing say_hi and "Bob", printing a greeting for "Bob".
  • The make_mult function creates a multiplier function based on a given factor.

Role of First-Class Functions in Decorators

  • Decorators receive the function to be decorated as an argument. This allows the decorator to modify or enhance the function's behavior.
  • Decorators return a new function that wraps the original function. This new function adds additional behavior before or after the original function is called.
  • When a function is decorated, it is assigned to the variable name of the original function. This means the original function is replaced by the decorated (wrapped) function.

Types of Decorators

1. Function Decorators:

The most common type of decorator, which takes a function as input and returns a new function. The example above demonstrates this type.

Python
def simple_decorator(func):
    def wrapper():
        print("Before calling the function.")
        func()
        print("After calling the function.")
    return wrapper

@simple_decorator
def greet():
    print("Hello, World!")

greet()

Output
Before calling the function.
Hello, World!
After calling the function.

Explanation:

  • simple_decorator(func): This decorator takes the function greet as an argument (func) and returns a new function (wrapper) that adds some functionality before and after calling the original function.
  • @simple_decorator: This is the decorator syntax. It applies the simple_decorator to the greet function.
  • Calling greet(): When greet() is called, it doesn't just execute the original function but first runs the added behavior from the wrapper function.

2. Method Decorators:

Used to decorate methods within a class. They often handle special cases, such as the self argument for instance methods.

Python
def method_decorator(func):
    def wrapper(self, *args, **kwargs):
        print("Before method execution")
        res = func(self, *args, **kwargs)
        print("After method execution")
        return res
    return wrapper

class MyClass:
    @method_decorator
    def say_hello(self):
        print("Hello!")

obj = MyClass()
obj.say_hello()

Output
Before method execution
Hello!
After method execution

Explanation:

  • method_decorator(func): The decorator takes the method (say_hello) as an argument (func). It returns a wrapper function that adds behavior before and after calling the original method.
  • wrapper(self, *args, **kwargs): The wrapper must accept self because it is a method of an instance. self is the instance of the class and *args and **kwargs allow for other arguments to be passed if needed.
  • @method_decorator: This applies the method_decorator to the say_hello method of MyClass.
  • Calling obj.say_hello(): The say_hello method is now wrapped with additional behavior.

3. Class Decorators

Class decorators are used to modify or enhance the behavior of a class. Like function decorators, class decorators are applied to the class definition. They work by taking the class as an argument and returning a modified version of the class.

Example:

Python
def fun(cls):
    cls.class_name = cls.__name__
    return cls

@fun
class Person:
    pass

print(Person.class_name) 

Output
Person

Explanation:

  • add_class_name(cls): This decorator adds a new attribute, class_name, to the class cls. The value of class_name is set to the name of the class (cls.__name__).
  • @add_class_name: This applies the add_class_name decorator to the Person class.
  • Result: When the Person class is defined, the decorator automatically adds the class_name attribute to it.
  • print(Person.class_name): Accessing the class_name attribute that was added by the decorator prints the name of the class, Person.

Common Built-in Decorators in Python

Python provides several built-in decorators that are commonly used in class definitions. These decorators modify the behavior of methods and attributes in a class, making it easier to manage and use them effectively. The most frequently used built-in decorators are @staticmethod, @classmethod, and @property.

@staticmethod

The @staticmethod decorator is used to define a method that doesn't operate on an instance of the class (i.e., it doesn't use self). Static methods are called on the class itself, not on an instance of the class.

Example:

Python
class MathOperations:
    @staticmethod
    def add(x, y):
        return x + y

# Using the static method
res = MathOperations.add(5, 3)
print(res)  

Output
8

Explanation:

  • add is a static method defined with the @staticmethod decorator.
  • It can be called directly on the class MathOperations without creating an instance.

@classmethod

The @classmethod decorator is used to define a method that operates on the class itself (i.e., it uses cls). Class methods can access and modify class state that applies across all instances of the class.

Example:

Python
class Employee:
    raise_amount = 1.05

    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    @classmethod
    def set_raise_amount(cls, amount):
        cls.raise_amount = amount

# Using the class method
Employee.set_raise_amount(1.10)
print(Employee.raise_amount)  

Output
1.1

Explanation:

  • set_raise_amount is a class method defined with the @classmethod decorator.
  • It can modify the class variable raise_amount for the class Employee and all its instances.

@property

The @property decorator is used to define a method as a property, which allows you to access it like an attribute. This is useful for encapsulating the implementation of a method while still providing a simple interface.

Example:

Python
class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value >= 0:
            self._radius = value
        else:
            raise ValueError("Radius cannot be negative")

    @property
    def area(self):
        return 3.14159 * (self._radius ** 2)

# Using the property
c = Circle(5)
print(c.radius) 
print(c.area)    
c.radius = 10
print(c.area)    

Output
5
78.53975
314.159

Explanation:

  • radius and area are properties defined with the @property decorator.
  • The radius property also has a setter method to allow modification with validation.
  • These properties provide a way to access and modify private attributes while maintaining encapsulation.

Chaining Decorators

In simpler terms chaining decorators means decorating a function with multiple decorators.

Example: 

Python
# code for testing decorator chaining 
def decor1(func): 
    def inner(): 
        x = func() 
        return x * x 
    return inner 

def decor(func): 
    def inner(): 
        x = func() 
        return 2 * x 
    return inner 

@decor1
@decor
def num(): 
    return 10

@decor
@decor1
def num2():
    return 10
  
print(num()) 
print(num2())

Output
400
200

Next Article
Article Tags :
Practice Tags :

Similar Reads