Decorators: it makes code stylish & simple.
decorators banner
5 Min Read

Decorators: it makes code stylish & simple.

Imagine if you had a magical spell that could enhance anything the way you want without changing any of its core behavior. This spell is known as a decorator in the realm of Design Patterns, which enhances any object.

Decorators are one of the must-have tools for the programmer's toolbox, which work like magic without compromising the core logic of what they are decorating. In this post, we are going to deep dive into this tool.

Let Us See What Is Decorator?

The decorator is a structural design pattern that allows behavior to be added to individual objects either statically or dynamically without affecting the behavior of the objects to which it is applied.

💡
The decorator pattern is useful because it allows you to adhere to the Single Responsibility Principle and Open/Closed Principle, by adding responsibilities to objects dynamically.
- Robert C. Martin (author of "Clean Code")

some of the common use cases of decorators are:

  • Logging
  • Authentication
  • Enforcing Types
  • Memoization
  • Performance Analysis of Functions
  • Rate Limiting

and more...

Python (Programming Language) as a high-level language use decorators to simplify its functionality for the developers below are some of them:

  • Function Decorators
    • timing - timing the execution of a function
    • logging - logging a function attribute for debugging purpose
    • caching - caching a function repeated calculated values (memorization technique).
  • Class Decorators
    • Singleton - Ensures that a class has only one instance
    • Some of the class-based decorators on the Frameworks Like: Django, Flask, FastAPI
  • Property Decorators
    • @property - Defines getters in class
    • @<attribute>.setter - Defines attribute setter methods in class
    • @<attribute>.delete - Defines attribute delete methods in class
  • Built-in Decorators
    • @staticmethod
    • @classmethod
    • @abstractmethod - Defines a method is abstract and it should be implemented in inheriting class.

Different Ways to use Decorators

Decorator as Function

Below bill generator function enhances functionality by generating a detailed bill for customers, incorporating state tax, central tax, and presenting a comprehensive summary of the customer's fruit purchase. This approach simplifies the billing process for the fruit shop and ensures an organized and structured output for customer transactions. In this Python code designed for a fruit shop, function decorators are used to create a streamlined billing system. The code includes a bill_generator function that acts as a decorator for the bill_calculator function. The bill_calculator function calculates the total cost of buying fruits based on their prices per kilogram. Introduction to Fruit Shop Billing System Using Function Decorators

from typing import List, Dict

STATE_TAX = 0.18
CENTRAL_TAX = 0.18
FRUITS_PRICE_PER_KG = {
    "apple": 200,
    "orange": 150,
    "banana": 40,
    "mango": 50,
    "grapes": 120,
    "pineapple": 120,
    "watermelon": 30,
    "kiwi": 80,
}


def bill_generator(func):
    def wrapper(fruits):
        total_cost = func(fruits)
        state_tax = STATE_TAX * total_cost
        central_tex = CENTRAL_TAX * total_cost
        print("*" * 75, end="\n")
        print("Sample Shop Name")
        print("*" * 75, end="\n")
        print("Item", "\t" * 4, "Weight", "\t" * 4, "Price")
        print("*" * 75, end="\n")
        for item, weight in fruits.items():
            print(item, "\t" * 4, weight, "\t" * 4, FRUITS_PRICE_PER_KG[item] * weight)
        print("*" * 75, end="\n")
        print("\t" * 10, f"Total:{total_cost}")
        print("\t" * 10, f"SGST:{state_tax}")
        print("\t" * 10, f"CSGT:{central_tex}")
        print("*" * 75, end="\n")
        print("\t" * 10, f"Grand Total:{round(total_cost+state_tax+central_tex,2)}")

    return wrapper


@bill_generator
def bill_calculator(fruits: List[Dict[str, float]]):
    total_cost = 0
    for item, weight in fruits.items():
        total_cost += FRUITS_PRICE_PER_KG[item] * weight
    return total_cost


# Input
customer_purchase = {"apple": 0.5, "orange": 1.25, "grapes": 0.6}
bill_calculator(customer_purchase)

# ***************************************************************************
# Sample Shop Name
# ***************************************************************************
# Item 				 Weight 				 Price
# ***************************************************************************
# apple 				 0.5 				 100.0
# orange 				 1.25 				 187.5
# grapes 				 0.6 				 72.0
# ***************************************************************************
# 										 Total:359.5
# 										 SGST:64.71
# 										 CSGT:64.71
# ***************************************************************************
# 										 Grand Total:488.92

Decorator as Function with arguments

In the above function what if we do not want to consume the tax rates & fruits prices from a global constant, instead you can dynamically pass them to the decorator. In that case we can use the decorator with arguments.

from typing import Dict, Callable


def bill_generator(
    state_tax: float, central_tax: float, fruits_price_per_kg: Dict[str, float]
) -> Callable:
    def bill_inner(func: Callable):
        def wrapper(fruits: Dict[str, float]):
            total_cost = func(fruits)
            state_tax_amount = state_tax * total_cost
            central_tax_amount = central_tax * total_cost
            print("*" * 75, end="\n")
            print("Sample Shop Name")
            print("*" * 75, end="\n")
            print("Item", "\t" * 4, "Weight", "\t" * 4, "Price")
            print("*" * 75, end="\n")
            for item, weight in fruits.items():
                print(
                    item, "\t" * 4, weight, "\t" * 4, fruits_price_per_kg[item] * weight
                )
            print("*" * 75, end="\n")
            print("\t" * 10, f"Total:{total_cost}")
            print("\t" * 10, f"SGST:{state_tax_amount}")
            print("\t" * 10, f"CSGT:{central_tax_amount}")
            print("*" * 75, end="\n")
            print(
                "\t" * 10,
                f"Grand Total:{round(total_cost + state_tax_amount + central_tax_amount,2)}",
            )

        return wrapper

    return bill_inner

# Input
state_tax = 0.18
central_tax = 0.18
fruits_price_per_kg = {
    "apple": 200,
    "orange": 150,
    "banana": 40,
    "mango": 50,
    "grapes": 120,
    "pineapple": 120,
    "watermelon": 30,
    "kiwi": 80,
}


@bill_generator(state_tax, central_tax, fruits_price_per_kg)
def bill_calculator(fruits: Dict[str, float]) -> float:
    total_cost = 0
    for item, weight in fruits.items():
        total_cost += fruits_price_per_kg[item] * weight
    return total_cost


# Input
customer_purchase = {"apple": 0.5, "orange": 1.25, "grapes": 0.6}
bill_calculator(customer_purchase)

# Output
# ***************************************************************************
# Item 				 Weight 				 Price
# ***************************************************************************
# apple 				 0.5 				 100.0
# orange 				 1.25 				 187.5
# grapes 				 0.6 				 72.0
# ***************************************************************************
# 										 Total:359.5
# 										 SGST:64.71
# 										 CSGT:64.71
# ***************************************************************************
# 										 Grand Total:488.92

Decorator as Class

Most use cases can be covered using function-based decorators, but in some cases, class-based decorators are more useful than function-based decorators.

  • State Management
    • Class-based decorators can help store information across multiple function calls, which is beneficial for preserving data beyond a single function call.
  • Complex Initializations
    • If a decorator needs complex initialization or has to take multiple arguments, using a class-based decorator can make the code more readable and organized.
  • Multiple Decorators with State
    • When you need to apply multiple decorators that share state or configuration, using classes can make the code cleaner and easier to manage.
  • Inheritance and Reuse
    • Class-based decorators can receive help from inheritance to create more complex hierarchies and reuse common functionality.

We will see an example of the state management decorator. Let us say we are developing an API, and we need to monetize it based on the number of times the API is accessed, and we need to limit it based on the customer limit. In this case, a state management API can be one of the solutions.

from random import randint
from typing import Callable

# API limits per day as per the account type
ACCOUNT_TYPE = {"Silver": 5, "Gold": 10, "Platinum": 15}

# Customer Database with id, name, account type and used count
# Let us assume this is the initial customer database
# Every day the used count will be reset to 0

CUSTOMERS = [
    {"id": 1, "name": "Anba", "account_type": "Silver", "used": 0},
    {"id": 2, "name": "Sunil", "account_type": "Gold", "used": 0},
    {"id": 3, "name": "Rohit", "account_type": "Platinum", "used": 0},
]


class CustomerValidator:
    # mimic the customer database
    def __init__(self, customer_id: int) -> None:
        self.customer_id = customer_id
        self.customer = CUSTOMERS[self.customer_id - 1]
        self.account_type = (
            self.customer["account_type"] if self.customer else None
        )  # type: account_type
        self.api_usage = self.customer["used"] if self.customer else None
        self.api_limit = ACCOUNT_TYPE.get(self.account_type)

    def __call__(self, func: Callable) -> Callable:
        def wrapper(*args, **kwargs):
            if self.api_limit is None:
                return "Invalid account type"
            if self.api_usage < self.api_limit:
                self.api_usage += 1
                CUSTOMERS[self.customer_id - 1]["used"] = self.api_usage
                return f"customer_name:{self.customer['name']} & Your Special Number is: {func(*args, **kwargs)}"
            else:
                return f"API Limit Exceeded. (Limit: {self.api_limit})"

        return wrapper


customer_id = 1


# function to get a special number
@CustomerValidator(customer_id)
def get_special_number():
    return randint(1, 100)


# now we are going to simulate 6 calls for the silver customer
for i in range(6):
    print(get_special_number())


# output
# customer_name:Anba & Your Special Number is: 48
# customer_name:Anba & Your Special Number is: 60
# customer_name:Anba & Your Special Number is: 4
# customer_name:Anba & Your Special Number is: 55
# customer_name:Anba & Your Special Number is: 64
# API Limit Exceeded. (Limit: 5)

Conclusion

Decorators are useful tools that can enhance our code by adding features without changing the current implementations. They simplify the code and reduce repetition, whether you are logging, enforcing access control, adding instrumentation, caching, or performing other tasks. Learning how to create and use decorators allows you to make the most of their advantages in your projects.


Resources

bytebyanbarasan-share/Python/Core at main · anbarasanv/bytebyanbarasan-share
blog post assets share location. Contribute to anbarasanv/bytebyanbarasan-share development by creating an account on GitHub.