Chapter 3: Understanding Decorators and their Applications

From this chapter onwards, we will start looking at various concepts that are part of metaprogramming along with examples of how to apply them. We will first take a look at decorators and how decorators can be implemented in Python 3.

Decorators are one of the metaprogramming concepts that deal with decorating a function without modifying the actual function body. As the name suggests, a decorator adds additional value to a function, a method, or a class by allowing the function to become an argument of another function that decorates or gives more information on the function, method, or class being decorated. Decorators can be developed on an individual user-defined function or on a method that is defined inside a class, or they can be defined on a class itself too. Understanding decorators will help us to enhance the reusability of functions, methods, and classes by manipulating them externally without impacting the actual implementation.

In the previous chapter, we reviewed the concept of object-oriented programming, which serves as the base for this chapter and the future chapters in this book.

In this chapter, we will be taking a look at the following main topics:

  • Looking into simple function decorators
  • Exchanging decorators from one function to another
  • Applying multiple decorators to one function
  • Exploring class decorators
  • Getting to know built-in decorators

By the end of this chapter, you should be able to create your own decorators, implement user-defined decorators on functions/methods and classes, and reuse built-in decorators.

Technical requirements

The code examples shared in this chapter are available on GitHub under the code for this chapter here: https://github.com/PacktPublishing/Metaprogramming-with-Python/tree/main/Chapter03.

Looking into simple function decorators

We will now look at different types of function decorators with an example. We will continue using the ABC Megamart example we looked at in the previous chapter. Each user-defined function in Python can perform a different operation. But what if we want different functions to show specific additional information, no matter what the functions perform? We can do this simply by defining another function that decorates any function that is provided as an input.

Let’s take a look at the following steps to understand this better:

  1. A function decorator can be defined as follows:

    def functiondecorator(inputfunction):  

        def decorator():  

            print("---Decorate function with this line---

              ")  

            return inputfunction()  

        return decorator  

This code defines a simple function decorator that takes in any input function as an argument and adds a line above the function result that prints ---Decorate function with this line--- as the first output line for any input function.

  1. This function decorator can be called by a new user-defined function with two different syntaxes. Let us define two simple functions:

    def userfunction1():  

        return "A picture is worth a thousand words "  

This function returns the phrase A picture is worth a thousand words.

  1. We will be adding one more function that returns a different phrase: Actions speak louder than words:

    def userfunction2():  

        return "Actions speak louder than words"  

  2. In the following step, let us add a function decorator to both the preceding user-defined functions and look at the results:

    decoratedfunction1 = functiondecorator(userfunction1)

    decoratedfunction2 = functiondecorator(userfunction2)

  3. In the preceding code, we have reassigned the functions by adding a decorator function to them. Executing decorated function 1 results in the following:

    decoratedfunction1()  

    ---Decorate function with this line---

    'A picture is worth a thousand words'

  4. Similarly, we can also execute decorated function 2:

    decoratedfunction2()

    ---Decorate function with this line---

    'Actions speak louder than words'

Both of the function results added an additional line, ---Decorate function with this line---, that was not part of their function definition but was part of the decorator function. These examples show the reusable nature of function decorators.

  1. Let us look further into syntax 2, which is the most widely used method of adding decorators to other functions, methods, or classes:

    @functiondecorator  

    def userfunction1():  

        return "A picture is worth a thousand words"  

    @functiondecorator  

    def userfunction2():  

        return "Actions speak louder than words"  

In the preceding code, while defining the user-defined functions, we added an additional line above the definition of @functiondecorator. This line signifies that we have added a decorator to the function in the definition stage itself. This decorator can be declared once and reused for any relevant function that is newly defined.

  1. Executing the preceding code provides the same results as in the code execution of examples with syntax 1:

    userfunction1()

    ---Decorate function with this line---

    'A picture is worth a thousand words'

    userfunction2()

    ---Decorate function with this line---

    'A picture is worth a thousand words'

Now that you understand simple function decorators, we can look into an example that demonstrates its applications.

Understanding function decorators with an application

We can further look into an example of function decorators using a scenario from ABC Megamart. In this example, we will create a function to add an email signature for a branch manager in a different format for each branch. We will define two functions, manager_albany and manager_manhattan, with different font colors and highlights.

Let’s look at this first piece of code:

def manager_albany(*args):  
    BLUE = '33[94m'  
    BOLD = '33[5m'  
    SELECT = '33[7m'
    for arg in args:
        print(BLUE + BOLD + SELECT + str(arg))
manager_albany('Ron D','[email protected]','123 Main Street','Albany','New York', 12084)  

The preceding code prints the branch manager’s email signature with white, bold, and blue highlighted text:

Ron D
123 Main Street
Albany
New York
12084

Now let’s take a quick look at this block of code:

def manager_manhattan(*args):
    GREEN = '33[92m'
    SELECT = '33[7m'
    for arg in args:
        print(SELECT + GREEN + str(arg))
manager_manhattan('John M',  '[email protected]', '40097 5th Main Street',   'Manhattan', 'New York City',  'New York',  11007)

This one prints the branch manager’s email signature with highlighted text:

John M
40097 5th Main Street
Manhattan
New York City
New York
11007

Now, let us add the name of ABC Megamart in both the signatures with a yellow highlight and modify the font color of the signature to yellow while keeping the signature highlight colors intact. To do this, we will create a function decorator that takes in the arguments of the preceding functions and add ABC Megamart with a black font and yellow highlight:

def signature(branch):  
    def footnote(*args):  
        LOGO = '33[43m'  
        print(LOGO + 'ABC Mega Mart')  
        return branch(*args)  
    return footnote  

The following figure is a representation of how an email signature decorator can be implemented on two different signatures.

Figure 3.1 – Email signature decorator

Figure 3.1 – Email signature decorator

The preceding signature decorator adds the name of ABC Megamart in both the signatures with a yellow highlight and modifies the font color of the signature to yellow while keeping the signature highlight colors intact.

First, let’s add @signature to manager_manhattan:

@signature
def manager_manhattan(*args):
    GREEN = '33[92m'
    SELECT = '33[7m'
    for arg in args:
        print(SELECT + GREEN + str(arg))
manager_manhattan('John M',  '[email protected]', '40097 5th Main Street',   'Manhattan', 'New York City',  'New York',  11007)

This code returns the following email signature:

ABC Mega Mart
John M
40097 5th Main Street
Manhattan
New York City
New York
11007

Now let’s add @signature to manager_albany:

@signature
def manager_albany(*args):  
    BLUE = '33[94m'  
    BOLD = '33[5m'  
    SELECT = '33[7m'
    for arg in args:
        print(BLUE + BOLD + SELECT + str(arg))
manager_albany('Ron D','[email protected]','123 Main Street','Albany','New York', 12084)  

Doing so returns the following email signature:

ABC Mega Mart
Ron D
123 Main Street
Albany
New York
12084

Adding a function decorator to different functions in the preceding code snippets makes them have common functionality – in this case, the ABC Megamart title with a yellow highlight as a common functionality while keeping the individual branch manager signatures. It’s a simple example of how reusable decorators can be and the nature of adding metadata or additional information to a function while keeping the actual functionality of the function intact.

Now that we understand what function decorators are and how we can use them, let’s look at utilizing decorators for different functions by exchanging them and making them more reusable.

Exchanging decorators from one function to another

We now have an understanding of what a function decorator is and how a function decorator can be used for more than one function. We will look into further exploring the reusability concept of decorators by creating two different decorators to serve two different purposes and later utilizing them by interchanging the decorators between different functions.

To demonstrate this concept, we will be creating Decorator 1 for function 1 and Decorator 2 for function 2, and then we will be exchanging them from one function to another. Let us create two decorators to decorate two different functions.

Decorator 1 will be created to convert a date argument that is provided as a holiday date to the function that sets holidays for the Alabama branch of ABC Megamart.

The following figure is a representation of Decorator 1 and its Function 1.

Figure 3.2 – Date converter as a decorator

Figure 3.2 – Date converter as a decorator

Let’s take a look at the code we’d be using for our desired example:

def dateconverter(function):  
    import datetime  
    def decoratedate(*args):     
        newargs = []  
        for arg in args:  
            if(isinstance(arg,datetime.date)):  
                arg = arg.weekday(),arg.day,arg.month,
                  arg.year  
            newargs.append(arg)  
        return function(*newargs)  
    return decoratedate    

The preceding dateconverter is a decorator function that takes in another function as an argument. To perform this function, we have imported the datetime library that helps us to convert the input date argument into the format of weekday, day of the month, month of the year, and year. This decorator function internally takes in all the arguments passed to the internal function and checks whether any of the function arguments are of the datetime data type, and if it finds a datetime object, it will be converted to display weekday, day of the month, month of the year, and year.

This decorator also stores the converted format of the datetime object along with the rest of the function arguments in a list and passes the list as an argument to the function that is provided as input to this decorator. Let us now create a function to set a holiday calendar for the Alabama branch and decorate it using this decorator function.

Function 1 is to set variables for the Alabama holiday calendar and it takes in the arguments using the *args parameter. The first argument of this function will be set as branch_id, the second argument as holiday_type, the third argument as holiday_name, and the fourth argument as holiday_date. All of these input arguments are converted into a dictionary variable by the function and it returns the dictionary with its key-value pairs denoting each value.

Here is what the code looks like using the details we just discussed:

@dateconverter  
def set_holidays_alabama(*args):  
    holidaydetails = {}  
    holidaydetails['branch_id'] = args[0]  
    holidaydetails['holiday_type'] = args[1]  
    holidaydetails['holiday_name'] = args[2]  
    holidaydetails['holiday_date'] = args[3]  
    return holidaydetails  

In the preceding code, we have started the function definition by adding the decorator @dateconverter, which takes care of converting the holiday date into the aforementioned format. Let us now call this function by providing the arguments required to create the holiday details dictionary:

from datetime import datetime  
holiday =datetime.strptime('2021-01-18', '%Y-%m-%d')  

In the preceding code, we have created a datatime object and stored it in a holiday variable that will be passed as one of the inputs to the set_holidays_alabama function:

set_holidays_alabama('id1000',  
                   'local',  
                   'Robert E. Lee's Birthday',  
                   holiday)  

The preceding code gives us the following decorated output:

{'branch_id': 'id1000',  
 'holiday_type': 'local',  
 'holiday_name': 'Robert E. Lee's Birthday',  
 'holiday_date': (0, 18, 1, 2021)}  

We can now go ahead and create another decorator that performs a different manipulation on another function that is provided as input.

Let’s now look at Decorator 2. The second decorator will be created to check whether the term id is present in the input that denotes that the input value is an identifier of any kind and returns the numerical value of the identifier by removing its prefix. This decorator will be added to a function to set promotion details for any input product for the Malibu branch.

The following figure is a representation of Decorator 2 and Function 2:

Figure 3.3 – ID identifier as a decorator

Figure 3.3 – ID identifier as a decorator

Here is the code we’ll be using for our decorator:

def identifier(function):  
    def decorateid(*args):     
        newargs = []  
        for arg in args:  
            if(isinstance(arg,str)):  
                arg = arg.lower()  
                if 'id' in arg:  
                    arg = int(''.join(filter(str.isdigit,
                      arg)))  
            newargs.append(arg)  
        return function(*newargs)  
    return decorateid   

The preceding identifier is a decorator function that takes in another function as an argument. This decorator function also internally takes in all the arguments passed to its internal function and navigates through each individual argument to check whether it is a string. If the argument is a string, the decorator converts the string into lowercase and checks whether it has a substring ID. If the substring ID is present in the variable, then all strings will be removed from the variable and only digits will be stored in it with the rest of the function arguments in a list, passing the list as an argument to the function that is provided as input to this decorator. Let us now create a function to set promotion details for the Malibu branch and decorate its ID using this decorator function.

Function 2 is to set variables for the product promotion details of the Malibu branch and it takes in the arguments using *args similar to the set_holidays_alabama function. The first argument of this function will be set as branch_id, the second argument as product_id, the third argument as promotion_date, the fourth as promotion_type, and the fifth as promotion_reason. All of these input arguments are also converted into a dictionary variable by the function and it returns the dictionary with its key-value pairs denoting each value. There are two id arguments in this function that get decorated by the identifier.

Here is what the code looks like using the details we just discussed:

@identifier  
def set_promotion_malibu(*args):  
    promotiondetails = {}  
    promotiondetails['branch_id'] = args[0]  
    promotiondetails['product_id'] = args[1]  
    promotiondetails['product_name'] = args[2]  
    promotiondetails['promotion_date'] = args[3]  
    promotiondetails['promotion_type'] = args[4]  
    promotiondetails['promotion_reason'] = args[5]  
    return promotiondetails  

In the preceding code, we have started the function definition by adding the decorator @identifier, which takes care of removing the prefixes from the id variable. Let us now call this function by providing the arguments required to create the product promotion details dictionary:

from datetime import datetime  
promotion_date = datetime.strptime('2020-12-23', '%Y-%m-%d')  

Here, we have created a datatime object and stored it in a promotion date, which will be passed as one of the inputs to the set_promotion_malibu function, but this date variable will stay in the same format as defined:

set_promotion_malibu('Id23400','ProdID201','PlumCake',promotion_date,'Buy1Get1','Christmas')  

The preceding code gives us the decorated output that follows:

{'branch_id': 23400,  
 'product_id': 201,  
 'product_name': 'plumcake',  
 'promotion_date': datetime.datetime(2020, 12, 23, 0, 0),  
 'promotion_type': 'buy1get1',  
 'promotion_reason': 'christmas'}  

We now have two decorators and two different functions decorated by them. To check whether these decorators can be exchanged, let us now redefine these functions by swapping the decorators using the following code:

@identifier  
def set_holidays_alabama(*args):  
    holidaydetails = {}  
    holidaydetails['branch_id'] = args[0]  
    holidaydetails['holiday_type'] = args[1]  
    holidaydetails['holiday_name'] = args[2]  
    holidaydetails['holiday_date'] = args[3]  
    return holidaydetails  
@dateconverter  
def set_promotion_malibu(*args):  
    promotiondetails = {}  
    promotiondetails['branch_id'] = args[0]  
    promotiondetails['product_id'] = args[1]  
    promotiondetails['product_name'] = args[2]  
    promotiondetails['promotion_date'] = args[3]  
    promotiondetails['promotion_type'] = args[4]  
    promotiondetails['promotion_reason'] = args[5]  
    return promotiondetails  

Let us input the required arguments and execute the preceding function, set_holidays_alabama:

from datetime import datetime  
holiday =datetime.strptime('2021-01-18', '%Y-%m-%d')  
set_holidays_alabama('id1000',  
                   'local',  
                   'Robert E. Lee's Birthday',  
                   holiday)  

This code gives us the decorated output as follows:

{'branch_id': 1000,  
 'holiday_type': 'local',  
 'holiday_name': 'robert e. lee's birthday',  
 'holiday_date': datetime.datetime(2021, 1, 18, 0, 0)}  

In the preceding output, the identifier is applied on the branch ID and there is no change to the holiday date. Similarly, let us execute the following code:

promotion_date = datetime.strptime('2020-12-23', '%Y-%m-%d')  
set_promotion_malibu('Id23400','ProdID201','PlumCake',promotion_date,'Buy1Get1','Christmas')  

This code gives us the decorated output that follows:

{'branch_id': 'Id23400',  
 'product_id': 'ProdID201',  
 'product_name': 'PlumCake',  
 'promotion_date': (2, 23, 12, 2020),  
 'promotion_type': 'Buy1Get1',  
 'promotion_reason': 'Christmas'}  

The following figure is a representation of how the two decorators will be exchanged or swapped between their functions:

Figure 3.4 – Exchange decorators

Figure 3.4 – Exchange decorators

Let us reuse the previous examples to look further into the concept of applying multiple decorators to one function.

Applying multiple decorators to one function

So far, we have understood that decorators can be created and added to functions to perform metaprogramming on the functions. We also understand that decorators can be reused and exchanged for different functions. We have also understood that decorators add decoration or value to a function from outside of the function body and help in altering the function with additional information. What if we want the function to perform two different actions through decorators and at the same time do not want the decorators to become more specific? Can we create two or more different decorators and apply them to a single function? Yes, we can. We will now look at decorating a function with more than one decorator and understand how it works.

For this example, let us reuse the decorators dateconverter and identifier. To understand this concept, we can reuse one of the previously declared functions, set_promotion_malibu, which has both a datetime object as an input argument – promotion date – and two ID values as input arguments – branch_id and product_id.

The following figure is a representation of adding two decorators to a function:

Figure 3.5 – Multiple decorators for one function

Figure 3.5 – Multiple decorators for one function

The following code puts our example into action:

@identifier  
@dateconverter  
def set_promotion_malibu(*args):  
    promotiondetails = {}  
    promotiondetails['branch_id'] = args[0]  
    promotiondetails['product_id'] = args[1]  
    promotiondetails['product_name'] = args[2]  
    promotiondetails['promotion_date'] = args[3]  
    promotiondetails['promotion_type'] = args[4]  
    promotiondetails['promotion_reason'] = args[5]  
    return promotiondetails  

In this code, we have added both decorators to the set_promotion_malibu function:

promotion_date = datetime.strptime('2021-01-01', '%Y-%m-%d')  
set_promotion_malibu('Id23400','ProdID203','Walnut Cake',promotion_date,'Buy3Get1','New Year')  

Executing the preceding code results in the application of both decorators on the input values:

{'branch_id': 23400,  
 'product_id': 203,  
 'product_name': 'walnut cake',  
 'promotion_date': (4, 1, 1, 2021),  
 'promotion_type': 'buy3get1',  
 'promotion_reason': 'new year'}  

From the preceding output, we can see that @identifier is applied on branch_id and product_id. At the same time, @dateconverter is applied on the promotion_date. Let us now explore other variants of decorators.

Exploring class decorators

A class decorator is similar to the function decorator that we discussed earlier. Class decorators can be used to decorate, modify behavior, or debug a function, similar to a function decorator, which adds behavior to a function without actually modifying the function itself. A class decorator can be defined as a class by using two of its default or built-in methods: __init__ and __call__. Any variable initialized as part of the __init__ function of a class while creating an object instance of the class becomes a variable of the class itself. Similarly, the __call__ function of a class returns a function object. If we want to use a class as a decorator, we need to make use of the combination of these two built-in methods.

Let us look at what happens if we don’t use the call method. Look at the following piece of code:

class classdecorator:  
    def __init__(self,inputfunction):  
        self.inputfunction = inputfunction  
      
    def decorator(self):  
        result = self.inputfunction()  
        resultdecorator = ' decorated by a class decorator'  
        return result + resultdecorator  

Here, we have created a class named classdecorator and have added the init method to take a function as input. We have also created a decorator method that stores the result of the initialized function variable and adds a decorator string decorated by a class decorator to the input function result.

Let us now create an input function to test the preceding classdecorator:

@classdecorator  
def inputfunction():  
    return 'This is input function'  

Adding this class decorator should decorate the input function. Let us check what happens when we call this input function:

inputfunction()

We get the following type error, which states classdecorator is not callable:

Figure 3.6 – Error due to an incorrect definition of the class decorator

Figure 3.6 – Error due to an incorrect definition of the class decorator

We are receiving this error since we did not use the right method to make the class behave as a decorator. The decorator method in the preceding code returns a variable but not a function. To make this class work as a decorator, we need to redefine the class as follows:

class classdecorator:  
    def __init__(self,inputfunction):  
        self.inputfunction = inputfunction  
      
    def __call__(self):  
        result = self.inputfunction()  
        resultdecorator = ' decorated by a class decorator'  
        return result + resultdecorator  

Here, we have replaced the decorator method with the built-in method __call__. Let us now redefine the input function and see what happens:

@classdecorator  
def inputfunction():  
    return 'This is input function'  

We can call the preceding function and check the behavior of this class decorator:

inputfunction()

'This is input function decorated by a class decorator'

The following figure is a simple representation that shows an incorrect way of creating a class decorator:

Figure 3.7 – Wrong method for creating a class decorator

Figure 3.7 – Wrong method for creating a class decorator

Here is the correct way of creating it:

Figure 3.8 – Correct method for creating a class decorator

Figure 3.8 – Correct method for creating a class decorator

Now that you have a better understanding of class decorator, we can proceed to analyze the application of class decorator on ABC Megamart.

Understanding class decorators with an application

We will look into a detailed example of the class decorator by applying it to a scenario on ABC Megamart. Let us consider a scenario where ABC Megamart has a separate class created for each branch. Let us also assume each class has its own method, buy_product, to calculate a product’s sales price by specifically applying the sales tax rate for the specific branch and product being purchased. When the mart wants to apply seasonal promotions that involve eight generic promotion types. Each branch class need not have a promotion calculation method to be applied to its calculated sales price. Instead, we can create a class decorator that can be applied to the buy_product method of each branch and the class decorator will, in turn, calculate the final sales price by applying promotion discounts on the actual sales price calculated by the branch.

We will create two classes and add the buy_product method to each class to calculate the sales price without adding a class decorator. This is to understand the return values of the actual methods:

class Alabama():  
      
    def buy_product(self,product,unitprice,quantity,
      promotion_type):  
        alabamataxrate = 0.0522  
        initialprice = unitprice*quantity   
        salesprice = initialprice + 
          initialprice*alabamataxrate  
        return salesprice, product,promotion_type  

Creating an object instance for the previous class and calling the method with its arguments returns the following result:

alb1 = Alabama()    
alb1.buy_product('Samsung-Refrigerator',200,1,'20%Off')   
 
(210.44, 'Samsung-Refrigerator', '20%Off')

Similarly, we can define the class Arizona and add the method buy_product and execute the following code to verify its return value without a decorator:

class Arizona():  
      
    def buy_product(self,product,unitprice,quantity,
      promotion_type):  
        arizonataxrate = 0.028  
        initialprice = unitprice*quantity   
        salesprice = initialprice + 
          initialprice*arizonataxrate  
        return salesprice, product,promotion_type  
arz1 = Arizona()  
arz1.buy_product('Oreo-Cookies',0.5,250,'Buy2Get1')  
(128.5, 'Oreo-Cookies', 'Buy2Get1')

The preceding buy_product method takes in product name, unit price, quantity, and promotion type as input and calculates the initial price by multiplying the unit price by the quantity of a product. It further calculates the sales price by adding the product of the initial price to the state tax rate along with the initial price calculated in the previous step. Finally, the method returns the sales price, product name, and promotion type. The sales tax rates are different for each state and the sales price calculation differs according to the sales tax rates.

We can now create a class decorator to apply a promotional discount on the sales price and calculate the final sales price for a product by including the offer rate or discount rate.

In the following code, let us define the class applypromotion and add two built-in methods required to make the class behave as a decorator:

  • The __init__ method: This is a function or method as an input variable in this scenario
  • The __call__ method: This method accepts multiple input arguments, which are also the arguments of the function or method being decorated

The input arguments are applied to the function or method being decorated and it further applies various discount rates to the sales price resulting from the input function by checking for eight different promotion types, recalculating the sales price, and storing it as the final sales price, as follows:

class applypromotion:  
    def __init__(self, inputfunction):  
        self.inputfunction = inputfunction  
                      
    def __call__(self,*arg):  
        salesprice, product,promotion_type = 
          self.inputfunction(arg[0],arg[1],arg[2],arg[3])  
        if (promotion_type == 'Buy1Get1'):  
            finalsalesprice = salesprice * 1/2  
        elif (promotion_type == 'Buy2Get1'):  
            finalsalesprice = salesprice * 2/3  
        elif (promotion_type == 'Buy3Get1'):  
            finalsalesprice = salesprice * 3/4  
        elif (promotion_type == '20%Off'):  
            finalsalesprice = salesprice - salesprice * 0.2  
        elif (promotion_type == '30%Off'):  
            finalsalesprice = salesprice - salesprice * 0.3  
        elif (promotion_type == '40%Off'):  
            finalsalesprice = salesprice - salesprice * 0.4  
        elif (promotion_type == '50%Off'):  
            finalsalesprice = salesprice - salesprice * 0.5  
        else:  
            finalsalesprice = salesprice   
        return "Price of - " + product + ": " + '$' + str(finalsalesprice)  

The class decorator to @applypromotion is now ready to be further used by other functions or methods. We can now apply this decorator to the buy_product method from the class Alabama:

class Alabama():  
    @applypromotion  
    def buy_product(product,unitprice,quantity,promotion_type):  
        alabamataxrate = 0.0522  
        initialprice = unitprice*quantity   
        salesprice = initialprice + initialprice*alabamataxrate  
        return salesprice, product,promotion_type  

Creating an object instance for the preceding code and calling its method works as follows:

alb = Alabama()  
alb.buy_product('Samsung-Refrigerator',200,1,'20%Off')  
'Price of - Samsung-Refrigerator: $168.352'

Similarly, we can also redefine the class Arizona and its method buy_product by adding the class decorator as follows:

class Arizona():  
    @applypromotion  
    def buy_product(product,unitprice,quantity,
      promotion_type):  
        arizonataxrate = 0.028  
        initialprice = unitprice*quantity   
        salesprice = initialprice + 
          initialprice*arizonataxrate  
        return salesprice, product,promotion_type  

Creating an object instance for the preceding code and calling its method works as follows:

arz = Arizona()  
arz.buy_product('Oreo-Cookies',0.5,250,'Buy2Get1')  
'Price of - Oreo-Cookies: $85.66666666666667'

Let us review the results of buy_product methods from Arizona before adding the decorator and after adding the decorator. The preceding code has the output after adding the decorator and the following code has the output before adding the decorator:

arz1.buy_product('Oreo-Cookies',0.5,250,'Buy2Get1')  
(128.5, 'Oreo-Cookies', 'Buy2Get1')

After adding the applypromotion decorator, the sales price for 250 packs of cookies is at a discounted rate of $85.66 compared to the price of $128.50 before applying the promotion. The store need not always add a promotion on a product and the buy_product method can reuse the applypromotion decorator only when it needs to sell a product on promotion, thus making the decorator externally alter the behavior of the class while keeping the buy_product method’s actual functionality intact.

The simple representation of this example is as follows:

Figure 3.9 – Class decorator to apply promotional discounts on products

Figure 3.9 – Class decorator to apply promotional discounts on products

Having learned how to apply class decorators to methods or functions from other classes, we will proceed further to look at some of the built-in decorators available in Python.

Getting to know built-in decorators

Now, the question is, do we have to always create user-defined or custom decorators to be applied to classes and methods, or do we have some pre-defined decorators that can be used for specific purposes.

In addition to the user-defined decorators that we’ve looked at throughout this chapter, Python has its own built-in decorators, such as @staticmethod and @classmethod, that can be directly applied to methods. These decorators add certain important functionalities to methods and classes during the process of the class definition itself. We will be looking at these two decorators in detail, as follows.

The static method

The static method@staticmethod – is a decorator that takes in a regular Python function as an input argument and converts it into a static method. Static methods can be created inside a class but will not use the implicit first argument of the class object instance usually denoted as an argument named self like the other instance-based methods.

To understand this concept, let us first create the class Alabama and add a function to the class buy_product without self as an argument and without the static method decorator and check its behavior:

class Alabama:  
    def buy_product(product,unitprice,quantity,promotion_type):  
        alabamataxrate = 0.0522  
        initialprice = unitprice*quantity   
        salesprice = initialprice + 
          initialprice*alabamataxrate  
        return salesprice, product,promotion_type  

Here we have defined the class Alabama with the function buy_product. Let us now create an object instance and call the function inside the class to check its behavior:

alb = Alabama()  
alb.buy_product('Samsung-Refrigerator',200,1,'20%Off')  

Executing this code leads to the following error:

Figure 3.10 – Error on calling a function without static method and self

Figure 3.10 – Error on calling a function without static method and self

Rerunning the preceding function without creating an object works as follows:

Alabama.buy_product('Samsung-Refrigerator',200,1,'20%Off')  
(210.44, 'Samsung-Refrigerator', '20%Off')

To avoid the preceding error and to call a function inside a class with or without creating an object, we can convert the function into a static method by adding the @staticmethod decorator to it. We can now look at how it works:

class Alabama:  
    @staticmethod  
    def buy_product(product,unitprice,quantity,
      promotion_type):  
        alabamataxrate = 0.0522  
        initialprice = unitprice*quantity   
        salesprice = initialprice + 
          initialprice*alabamataxrate  
        return salesprice, product,promotion_type  
      
    def another_method(self):  
        return "This method needs an object"  

We have added an additional method named another_method, which can only be called using an object instance. Let us now create an object for the class and call both the preceding methods:

albstatic = Alabama()  
albstatic.buy_product('Samsung-Refrigerator',200,1,'20%Off')  
(210.44, 'Samsung-Refrigerator', '20%Off')  
  
albstatic.another_method()  
'This method needs an object'  

Both the methods, static and instance, can be called using the object of the class. At the same time, the static method can also be called using the class itself without creating an object:

Alabama.buy_product('Samsung-Refrigerator',200,1,'20%Off')  
(210.44, 'Samsung-Refrigerator', '20%Off')  
  
Alabama.another_method()  

Executing this code leads to the following error:

Figure 3.11 – Error on calling an instance method using its class

Figure 3.11 – Error on calling an instance method using its class

The static method generated the expected output when called using its class, while the instance method did not run. This is the advantage of using a static method to convert a function into a method inside a class.

The class method

The class method@classmethod – is also a built-in decorator similar to @staticmethod, and this decorator also converts a function into a static method inside a class. @staticmethod does not have an implicit argument of the object to a class whereas @classmethod has an implicit argument, cls, which gets added to the function, while the @classmethod decorator is added to it as seen in the following code block:

class Alabama:  
    @classmethod  
    def buy_product(cls,product,unitprice,quantity,
      promotion_type):  
        alabamataxrate = 0.0522  
        initialprice = unitprice*quantity   
        salesprice = initialprice + 
          initialprice*alabamataxrate  
        return cls,salesprice, product,promotion_type  

This function can be called either with or without creating a class instance. We can look at both in the following code:

Alabama.buy_product('Samsung-Refrigerator',200,1,'20%Off')  
(__main__.Alabama, 210.44, 'Samsung-Refrigerator', '20%Off')  
  
alb = Alabama()  
alb.buy_product('Samsung-Refrigerator',200,1,'20%Off')  
(__main__.Alabama, 210.44, 'Samsung-Refrigerator', '20%Off')  

In the preceding code, we can see that a function converted by @classmethod into a class method can be called directly using the class or by creating an object of the class.

These are a few of the built-in decorators and there are more such decorators available in Python 3 that can be explored and reused.

Summary

In this chapter, we have learned how to create simple decorators and how to apply decorators with examples. We saw how to exchange decorators from one function to another along with how to add multiple decorators to one function.

We now understand the concept of class decorators and have looked at an example of how to apply them. And finally, we learned how to use some built-in decorators such as @staticmethod and @classmethod.

All of these concepts are part of Python metaprogramming and they are used to change the behavior of a function or a method externally and without impacting the internal functionalities of the function or method.

In the next chapter, we will be looking at the concept of meta classes with different examples.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset