Python – Decorators

By | 25/09/2024

In this post, we’ll see what Decorators are, look at some built-in decorators, and see how to create our own custom decorator.
But first of all, what is a Decorator?
A decorator is a design pattern in Python that allows us to add new functionality to our existing object without modifying its structure. Decorators are implemented as functions that take another function as an argument, extend its behavior, and return a new function with the extended behavior.
In a nutshell, a decorator wraps another function and allows us to execute code before and after the wrapped function runs, without modifying the function itself.


Python comes with several built-in decorators that are commonly used in object-oriented programming and some of them are:
@staticmethod – It turns a method into a static method, which means it doesn’t receive the implicit self argument.

[MATHOPERATION.PY]

class MathOperation:
    @staticmethod
    def sum(a, b):
        return a+b

    def multiplication(self, a, b):
        return a*b

[MAIN.PY]

from MathOperation import MathOperation

objMath = MathOperation()

print(f"The result of 5*5 is {objMath.multiplication(5,5)}")

print(f"The result of 5+5 is {MathOperation.sum(5,5)}")


@classmethod – It is used to define a method that’s bound to the class and not the instance of the class.
One of the most common uses, is to create instance of a class using different types of data.

[PERSON.PY]

import datetime

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @classmethod
    def from_birth_year(cls, name, birth_year):
        current_year = datetime.datetime.now().year
        age = current_year - birth_year
        return cls(name, age)

[MAIN.PY]

from Person import Person

objPerson = Person("Damiano1", 33)
objPerson2 = Person.from_birth_year("Damiano2", 1991)

print(objPerson.age)
print(objPerson2.age)


@property – It allows us to define methods in a class that can be accessed like attributes.
Moreover, it allows us to add logic when accessing or setting an attribute without changing the external interface of our class.

[RECTANGLE.PY]

class Rectangle:
    def __init__(self, width, height):
        self.width = width  # Calls the width setter
        self.height = height  # Calls the height setter

    @property
    def width(self):
        return self._width

    @width.setter
    def width(self, value):
        if value <= 0:
            raise ValueError("Width must be positive.")
        self._width = value

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value):
        if value <= 0:
            raise ValueError("Height must be positive.")
        self._height = value

    @property
    def area(self):
        return self.width * self.height

[MAIN.PY]

from Rectangle import Rectangle

objRect = Rectangle(5, 10)
print(
    f"The area of the Rectangle ({objRect.width} - {objRect.height}) is :{objRect.area}")

objRect.width = 7
print(
    f"The area of the Rectangle ({objRect.width} - {objRect.height}) is :{objRect.area}")


objRect.width = -7
print(
    f"The area of the Rectangle ({objRect.width} - {objRect.height}) is :{objRect.area}")


@abstractmethod – This decorator from the “abc” module indicates that a method must be overridden in a subclass, effectively making the class abstract.

[VEHICLE.PY]

from abc import ABC, abstractmethod


class Vehicle(ABC):
    @abstractmethod
    def start_engine(self):
        pass

[CARFERRARI.PY]

from Vehicle import Vehicle


class CarFerrari(Vehicle):
    def start_engine(self):
        print("Start from Ferrari engine")

[CARFORD.PY]

from Vehicle import Vehicle


class CarFord(Vehicle):
    def start_engine(self):
        print("Start from Ford engine")

[MAIN.PY]

from CarFerrari import CarFerrari
from CarFord import CarFord

carFerrari = CarFerrari()
carFord = CarFord()

carFerrari.start_engine()
carFord.start_engine()


@dataclass – It automatically adds special methods like __init()__ to the class.

[EMPLOYEE.PY]

from dataclasses import dataclass


@dataclass
class Employee:
    name: str
    position: str
    salary: float

    def print_info(self):
        print(
            f"Employee info: name='{self.name}', position='{self.position}', salary={self.salary}")

[MAIN.PY]

from Employee import Employee

objEmp = Employee('Damiano', 'Director', 120000)

objEmp.print_info()



CUSTOM DECORATOR – Finally, we will see how to create a custom Decorator called @timer, to calculate and display the time a function takes to execute.

[TIMER.PY]

import time  # Import the time module to work with time-related functions


def timer(func):
    """Decorator that measures the execution time of a function."""
    def wrapper(*args, **kwargs):
        # Record the start time before the function execution
        start_time = time.time()
        # Call the original function with all its arguments
        result = func(*args, **kwargs)
        # Record the end time after the function execution
        end_time = time.time()
        # Calculate the elapsed time and print it
        print(f"{func.__name__} took {end_time - start_time:.6f} seconds to execute.")
        # Return the result obtained from the original function
        return result
    # Return the wrapper function to replace the original function
    return wrapper

[MAIN.PY]

from timer import timer  # Import the 'timer' decorator from the 'timer' module


@timer  # Apply the 'timer' decorator to the 'compute_sum' function
def compute_sum(n):
    """Compute the sum of numbers from 0 to n-1."""
    total = sum(range(n))  # Calculate the sum of numbers from 0 to n-1
    return total  # Return the computed total


# Usage
result = compute_sum(1000000)  # Call 'compute_sum' with n=1,000,000

print(result)  # Print the result to the console



Leave a Reply

Your email address will not be published. Required fields are marked *