Enabling Code Reuse with Decorators – Part 3

Welcome to the 3rd part of my python decorator series. In my first and second posts,I introduced you to function and class decorators.
In this post, I am going to show you how to write decorators that receive an argument.

OUTLINE OF DECORATOR SERIES
i. Introduction to decorators and decorating python functions.
ii.Writing class decorators and decorating class methods.
iii. Decorators that take arguments (this article).

Function Decorators with Arguments

Let us start with the basic syntax of decorators that receives an argument.

from functools import wraps

def decorator_factory(*decorator_arg, **decorator_kwargs):
    def decorate(function):
        @wraps(function)
        def wrapper(*args, **kwargs):
            if (decorator_arg or decorator_kwargs):
                print (f"Your decorator has the arguments {decorator_arg} \
                decorator_kwargs}")
            result = function(*args, **kwargs)
            return result     
        return wrapper
    return decorate

At first glance, the implementation looks tricky, but the idea is relatively simple.
The outermost function decorator_factory()accepts the desired arguments and simply makes them available to the inner functions of the decorator.
The inner function   decorate()accepts a function and puts a wrapper around it.
The key part is that the wrapper is allowed to use the arguments passed to decorator_factory().

In summary
Decorators with arguments should return a function that will take a function and return another function.

Let’s see our decorator in action.

@decorator_factory("a")
def greet(greetings):
     return greetings

print(greet("Bonjour!"))
print("================================")

@decorator_factory("a", "b",c=1)
def greet(greetings):
     return greetings

greet("Bonjour!")
Your decorator has the arguments ('a',) ,{}

'Bonjour!'
================================

Your decorator has the arguments ('a','b') ,{'c':1} 

'Bonjour!'

The syntax above could be written as:

def greet(greetings):
     return greetings

print(decorator_factory("a")(greet)("Bonjour!"))
decorator_factory("a","b",c=1)(greet)("Bonjour!")
Your decorator has the arguments ('a',) ,{} 
'Bonjour!'

Your decorator has the arguments ('a','b') ,{'c':1}

'Bonjour!'

You notice our decorator_factory() is very flexible, it can either take positional arguments or keywords.

A Simple Real Life Example

A pizza store is having its 6th anniversary, they are running a discount promo of 25% and offering a free box of chocolates to customers who purchase above ₦4000.
As a developer working for the company, you don’t want to modify the existing codebase, so we create a promo decorator.

from functools import wraps

pizza = {"Neapolitan Pizza": 4500, "Chicago Pizza":2000, \
         "New York-Style Pizza":3000}



def promo(discount_code , gift):
    def pizza_promo(function):
        @wraps(function)
        def wrapper(*args, **kwargs):
            result = function(*args, **kwargs)
            if result > 4000:
                discount = discount_code * result
                order = result - discount 
                result = f"Dear customer, your total order is ₦{order} " +\
                          f"and you just had a 25% discount and a free {gift}"
                return result
            else:
                return "Dear customer, you are not qualified for our promo"       
        return wrapper
    return pizza_promo



@promo(discount_code = 0.25, gift= 'box of chocolates')
def make_order(pizza_type, order_qty):
    total_order = pizza[pizza_type] * order_qty
    return total_order

print (make_order("New York Style Pizza",2))
print(make_order("Chicago Pizza",1))
Dear customer, your total order is ₦4500.0 and you just had a 25% discount and a free box of chocolates
Dear customer, you are not qualified for our promo

Class-Based Decorator with Arguments

Let us start with the basic syntax.

import functools 


class ClassDecorator:
    def __init__(self, *class_args, **class_kwargs):
        self.class_args = class_args
        self.class_kwargs = class_kwargs
         
       
    def __call__(self, func):
        self.func = func        
        functools.update_wrapper(self, func)
        def wrapper(*args, **kwargs):
            #do something..............
            result = func(*args, **kwargs)
            return result
        return wrapper
    
    def __get__(self, instance,cls):
        if instance is None:
            return self
        else:
            return types.MethodType(self, instance)

From our previous post on the python decorator, to define a class decorator, we need to make sure it implements the__call__() and __get__() method.
You notice our __call__() method has a nested function(wrapper() and it returns it).

 

Let us rewrite our promo decorator as a class decorator.

class PizzaPromo:
    def __init__(self, discount_code,gift):
        self.discount_code = discount_code
        self.gift = gift
         
       
    def __call__(self, func):
        self.func = func
        # functools.wraps for classes...         
        functools.update_wrapper(self, func)
        def wrapper(*args, **kwargs):
            result = self.func(*args, **kwargs)
            if result > 4000:
                discount = self.discount_code * result
                order = result - discount 
                result = f"Dear customer, your total order is ₦{order} " +\
                          f"and you just had a 25% discount and a free \
                           {self.gift}"
                return result
            else:
                return "Dear customer, you are not qualified for our promo"
        return wrapper
    
    def __get__(self, instance,cls):
        if instance is None:
            return self
        else:
            return types.MethodType(self, instance)
@PizzaPromo(discount_code = 0.25, gift= 'box of chocolates')
def make_order(pizza_type, order_qty):
    total_order = pizza[pizza_type] * order_qty
    return total_order

print(make_order("New York-Style Pizza",2))
'Dear customer, your total order is ₦4500.0 and you just had a 25% discount and a free box of chocolates'

 

 

💃💃Yay!, we have come to the end of our decorator series, thanks for visiting my blog

 

Leave a Reply

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