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