Chapter 8: Defining Templates for Algorithms

In this chapter, we will look at what templates are and how to implement template programming in Python.

What are templates and where are they useful? The main usage of applying the concepts of metaprogramming during the process of developing an application is to design a reusable framework that can be manipulated externally through the programming of metadata of Python objects rather than modifying the object itself. Templates, as the name suggests, can act as a template, a format, or a model on how a sequence of operations can be performed on a Python object. These templates can be used to define the common functionalities of methods within a class and to reuse them through the application of the object-oriented programming concept of inheritance.

Throughout this chapter, we will look at understanding how templates can be defined and used in Python and how a sequence of common operations can be designed into a template that fits into a framework. Speaking of designs, template programming is one of the main concepts within the design patterns of Python. Design patterns will be covered in detail in Chapter 12 on design patterns.

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

  • Explaining a sequence of operations
  • Defining the sequence of methods
  • Identifying the common functionalities
  • Designing templates

By the end of this chapter, you should be able to apply generics and type checking on Python variables. You should also be able to create your own domain-specific data types.

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/Chapter08.

Explaining a sequence of operations

Developing algorithms is always interesting, especially in a language like Python where less code needs to be written to complete an action compared to any other programming language. An algorithm is a simple sequence of steps that need to be performed to accomplish a task. While developing any algorithm, the most important aspect is to ensure that we are following the steps to perform the action in the right sequence. This section covers examples of a sequence of operations and how they can be defined in a Python program.

Back to our core example

In this chapter, we will continue using our core example of ABC Megamart, and we will specifically look at the billing counter where we can perform a sequence of operations. The reason we are focusing on a sequence of operations here is to especially understand how templates can be utilized to perform a set of tasks, and also how they can be reused to perform similar kinds of other tasks too. So, let’s begin.

At ABC Megamart, we have four different checkout counters to check out the shopping items from the cart. The details of the counters are as follows:

  • The first one is to check out items that contain vegetables and dairy.
  • The second one is to check out items that contain less than 10 assorted items, excluding electronics, vegetables, and dairy.
  • The third one is to check out items that contain more than 10 assorted items, excluding electronics, vegetables, and dairy.
  • The fourth one is to check out electronic goods.

Each of these counters is performing a sequence of operations and at this point in time, they might look like they are an independent set of operations. The goal of this chapter is to create templates and look at a common way of connecting these independent operations. To connect them and create a template, we need to understand the sequence of operations in each of these counters.

Let’s now look at what each of the counters will work on.

The vegetables and dairy counter

The journey of a customer to the billing counter starts from the vegetable section, where vegetables are added to the shopping cart, the customer then stands in a queue at the respective billing counter, vegetables and fruit are weighed and packed, a price tag with a bar code is added on the packet, the bar code is scanned and the bill is added to the invoice for each item, a tax component is added for each item, and the bill is totaled, printed, and handed over to the customer, who then pays the bill.

The graphical representation of the steps is as follows:

Figure 8.1 – Vegetables counter

Figure 8.1 – Vegetables counter

The following functions will be defined to perform each of these operations:

return_cart()
goto_vege_counter()
weigh_items()
add_price_tag()
scan_bar_code()
add_billing()
add_tax()
calc_bill()
print_invoice()
receive_payment()

Let’s further look at the next counter, which handles less than 10 items.

Less than 10 items counter

When a customer adds less than 10 items to the cart and the items do not contain vegetables, fruit, dairy, or electronics, then the customer goes to the less than 10 items counter where the bar code on each item is scanned and the bill is added to the invoice for each item, a tax component is added for each item, and the bill is totaled, printed, and handed over to the customer, who then pays the bill.

The graphical representation of the steps is as follows:

Figure 8.2 – Less than 10 items counter

Figure 8.2 – Less than 10 items counter

The following functions will be defined to perform each of these operations:

return_cart()
goto_less_t10_counter()
review_items()
count_items()
scan_bar_code()
add_billing()
add_tax()
calc_bill()
print_invoice()
receive_payment()

Let’s further look at the next counter, which handles more than 10 items.

The greater than 10 items counter

When a customer adds more than 10 items to the cart and the items do not contain vegetables, fruit, dairy, or electronics, then the customer goes to the greater than 10 items counter where the bar code on each item is scanned and the bill is added to the invoice for each item, coupons are applied, a tax component is added for each item, and the bill is totaled, printed, and handed over to the customer, who then pays the bill.

The graphical representation of the steps is as follows:

Figure 8.3 – Greater than 10 items counter

Figure 8.3 – Greater than 10 items counter

The following functions will be defined to perform each of these operations:

return_cart()
gotoGreatT10Counter()
review_items()
count_items()
scan_bar_code()
add_billing()
apply_coupon()
add_tax()
calc_bill()
print_invoice()
receive_payment()

Let’s further look at the next counter, which handles electronic items.

Electronics counter

The last counter is the electronics counter, where a customer goes to the counter, gets the electronic items tested, the item is scanned, and the bill is added to the invoice for each item. A tax component is added for each item and the bill is totaled, printed, and handed over to the customer, who then pays the bill.

The graphical representation of the steps is as follows:

Figure 8.4 – Electronics counter

Figure 8.4 – Electronics counter

The following functions will be defined to perform each of these operations:

return_cart()
goto_electronics_counter()
review_items()
test_electronics()
scan_bar_code()
add_billing()
apply_coupon()
add_tax()
calc_bill()
print_invoice()
receive_payment()

In each of the preceding billing counters, we looked at the sequence of operations that happens for a sale to complete.

With this understanding, let’s look at defining each of the operations into methods in the following section.

Defining the sequence of methods

Defining the methods helps us in understanding each of the operations performed at each counter in detail. Let’s define the classes and methods required to fulfill the actions to be performed in each operation. We will be covering the following counters in this section:

  • The vegetable counter
  • Less than 10 items counter
  • Greater than 10 items counter
  • The electronics counter

Let’s begin with the vegetable counter.

The vegetable counter

The following are the steps for the operation of this counter:

  1. We will first create the VegCounter class as follows:

    class VegCounter():

  2. In the following code, we will be defining the return_cart method that returns the list of items added to the shopping cart:

        def return_cart(self,*items):

            cart_items = list(items)

            return cart_items

  3. Let’s now return the name of the counter to be included in the bill. For this example, the counter name is Vegetables & Dairy:

        def goto_vege_counter(self):

            return 'Vegetables & Dairy'

  4. In the following code, let’s define the method to weigh the items in the cart and return a dictionary of items and their corresponding weights:

        def weigh_items(self,*weights,cart_items = None):

            weight = list(weights)

            item_weight = dict(zip(cart_items, weight))

            return item_weight

  5. Next, let’s define a method to take the unit price and weights as input and calculate the price of each item by multiplying the weights and unit price:

        def add_price_tag(self,*units,weights = None):

            pricetag = []

            for item,price in zip(weights.items(),list(units)):

                pricetag.append(item[1]*price)

            return pricetag        

  6. In the following method, let’s input bar codes to each of the items in the cart and return the bar codes as a list:

        def scan_bar_code(self,*scan):

            codes = list(scan)

            return codes

  7. Next, let’s add a method to add price tags to the bar codes by creating a dictionary object and adding the codes and their corresponding price tags as key-value pairs:

        def add_billing(self,codes=None,pricetag=None):

            self.codes = codes

            self.pricetag = pricetag

            bill = dict(zip(self.codes, self.pricetag))

            return bill

  8. Then, let’s add tax percentages for each of the items and return the tax values as a list:

        def add_tax(self,*tax):

            taxed = list(tax)

            return taxed

  9. Let’s further use the price tags and the tax values and calculate the bill for each of the items in the cart, and create a dictionary to add the items and their corresponding billing amount:

        def calc_bill(self,bill,taxes,cart_items):

            items = []

            calc_bill = []

            for item,tax in zip(bill.items(),taxes):

                items.append(item[1])

                calc_bill.append(item[1] + item[1]*tax)

            finalbill = dict(zip(cart_items, calc_bill))

            return finalbill

  10. In the following method, let’s print the invoice with the counter name, items in the cart, price, and the total bill amount:

        def print_invoice(self,finalbill):

            final_total = sum(finalbill.values())

            print('**************ABC Megamart*****************')

            print('***********------------------**************')

            print('Counter Name: ', self.goto_vege_counter())

            for item,price in finalbill.items():

                print(item,": ", price)

            print('Total:',final_total)

            print('***********------------------**************')

  11. Then, let’s print the invoice with a statement stating that the invoice is paid:

        def receive_payment(self,finalbill):

            final_total = sum(finalbill.values())

            print('**************ABC Megamart*****************')

            print('***********------------------**************')

            print('Counter Name: ', self.goto_vege_counter())

            for item,price in finalbill.items():

                print(item,": ", price)

            print('Total:',final_total)

            print('***********------------------**************')

            print('***************PAID************************')

  12. Executing the preceding code results in the following. The methods are called in a sequence so that the results from one method are provided as input to the next step:

    veg = VegCounter()

    cart = veg.return_cart('onions','tomatoes','carrots','lettuce')

    item_weight = veg.weigh_items(1,2,1.5,2.5,cart_items = cart)

    pricetag = veg.add_price_tag(7,2,3,5,weights = item_weight)

    codes = veg.scan_bar_code(113323,3434332,2131243,2332783)

    bill = veg.add_billing(codes,pricetag)

    taxes = veg.add_tax(0.04,0.03,0.035,0.025)

    finalbill = veg.calc_bill(bill,taxes,cart)

    veg.print_invoice(finalbill)

The output of the printed invoice looks as follows:

**************ABC Megamart*****************

***********------------------**************

Counter Name:  Vegetables & Dairy

onions :  7.28

tomatoes :  4.12

carrots :  4.6575

lettuce :  12.8125

Total: 28.87

***********------------------**************

  1. Next, let’s print the invoice that has been paid by the customer, veg.receive_payment(finalbill).

The output of the paid invoice looks as follows:

**************ABC Megamart*****************

***********------------------**************

Counter Name:  Vegetables & Dairy

onions :  7.28

tomatoes :  4.12

carrots :  4.6575

lettuce :  12.8125

Total: 28.87

***********------------------**************

***************PAID************************

Less than 10 items counter

Similar to the class defined for the vegetable counter, we can also define the methods for the remaining three counters. The detailed code for the remaining counters is available at https://github.com/PacktPublishing/Metaprogramming-with-Python/tree/main/Chapter08.

For the code for this counter, let’s create the LessThan10Counter class and add all of its methods, which includes return_cart, goto_less_t10_counter, review_items, count_items, scan_bar_code, add_billing, add_tax, calc_bill, print_invoice, and receive_payment. For simplicity, let’s look at the additional methods that we have in each counter instead of repeating all of the methods:

  1. Let’s start by creating the LessThan10Counter class:

    class LessThan10Counter():

    …    

  2. In this class, we have a goto_less_t10_counter method, which returns the name of the counter:

        def goto_less_t10_counter(self):

              return 'Less than 10 counter'

  3. We also have the following method to review the items in the cart to make sure that they are not electronic, vegetable, fruit, or dairy products:

         def review_items(self,item_type = None):

            veg_cart = ['Vegetables', 'Dairy', 'Fruits']

            if (item_type == 'Electronics'):

                print("Move to Electronics Counter")

            elif (item_type in veg_cart):        

                print("Move to Vege Counter")

  4. In the following method, let’s count the items to make sure that the total number of items in the cart is less than 10:

        def count_items(self,cart_items = None):

            if len(cart_items)<=10:

                print("Move to Less than 10 items counter")

            else:

                print("Move to Greater than 10 items counter")

        …

  5. Executing all of the methods for this class in a sequence results in the following:

    less10 = LessThan10Counter()

    cart = less10.return_cart('paperclips','blue pens','stapler','pencils')

    less10.review_items(item_type = ['stationary'])

    less10.count_items(cart)

    codes = less10.scan_bar_code(113323,3434332,2131243,2332783)

    bill = less10.add_billing(10,15,12,14,codes = codes)

    taxes = less10.add_tax(0.04,0.03,0.035,0.025)

    finalbill = less10.calc_bill(bill,taxes,cart)

    less10.print_invoice(finalbill)

    less10.receive_payment(finalbill)

The output for the paid invoice looks as follows:

**************ABC Megamart*****************

***********------------------**************

Counter Name:  Less than 10 counter

paperclips :  10.4

blue pens :  15.45

stapler :  12.42

pencils :  14.35

Total: 52.620000000000005

***********------------------**************

***************PAID************************

Greater than 10 items counter

In this section, let’s define the class and methods for the counter for greater than 10 items.

For the code here, let’s create the GreaterThan10Counter class and add all of its methods, which includes return_cart, goto_greater_t10_counter, review_items, count_items, scan_bar_code, add_billing, add_tax, apply_coupon, calc_bill, print_invoice, and receive_payment. For simplicity, let’s look at the additional methods that we have in each counter instead of repeating all of the methods:

  1. We will first create the GreaterThan10Counter class:

    class GreaterThan10Counter():

  2. In this class, we have a goto_greater_t10_counter method counter that returns the name of the counter:

        def goto_greater_t10_counter(self):

            return 'Greater than 10 counter'

    …   

  3. Next, let’s add a method to apply a discount coupon to the items purchased:

        def apply_coupon(self):

            coupon_discount = 0.1

            return coupon_discount        

       …     

  4. Executing all of the methods for this class in a sequence results in the following:

    greater = GreaterThan10Counter()

    cart = greater.return_cart('paper clips','blue pens','stapler','pencils','a4paper','a3paper','chart',

                              'sketch pens','canvas','water color','acrylic colors')

    greater.review_items(item_type = ['stationary'])

    greater.count_items(cart)

    codes = greater.scan_bar_code(113323,3434332,2131243,2332783)

    bill = greater.add_billing(10,15,12,14,codes = codes)

    taxes = greater.add_tax(0.04,0.03,0.035,0.025)

    greater.apply_coupon()

    finalbill = greater.calc_bill(bill,taxes,cart)

    greater.print_invoice(finalbill)

    greater.receive_payment(finalbill)

The output for the paid invoice looks as follows:

**************ABC Megamart*****************

***********------------------**************

Counter Name:  Greater than 10 counter

paper clips :  10.4

blue pens :  15.45

stapler :  12.42

pencils :  14.35

Total: 47.358000000000004

***********------------------**************

***************PAID************************

In this class, we had a different method definition for goto_greater_t10_counter and a new apply_coupon method.

The electronics counter

In this section, let’s define the class and methods for the electronic items counter. In the following code, let’s create the ElectronicsCounter class and add all of its methods, which includes return_cart, goto_electronics_counter, review_items, test_electronics, scan_bar_code, add_billing, add_tax, apply_coupon, calc_bill, print_invoice, and receive_payment. For simplicity, let’s look at the additional methods that we have in each counter instead of repeating all of the methods:

  1. We will first create the class for the electronics counter:

    class ElectronicsCounter():

  2. In this class, we have a method to go to the electronics counter that returns the name of the counter:

        def goto_electronics_counter(self):

            return 'Electronics counter'

  3. Next, let’s define a method that provides the status of the electronic goods and checks whether they are working:

          def test_electronics(self,*status):

            teststatus = list(status)

            return teststatus            

  4. Executing all of the methods for this class in a sequence results in the following:

    electronics = ElectronicsCounter()

    cart = electronics.return_cart('television','keyboard','mouse')

    electronics.review_items(item_type = ['Electronics'])

    electronics.test_electronics('pass','pass','pass')

    codes = electronics.scan_bar_code(113323,3434332,2131243)

    bill = electronics.add_billing(100,16,14,codes = codes)

    taxes = electronics.add_tax(0.04,0.03,0.035)

    electronics.apply_coupon()

    finalbill = electronics.calc_bill(bill,taxes,cart)

    electronics.print_invoice(finalbill)

    electronics.receive_payment(finalbill)

The output for the paid invoice looks as follows:

**************ABC Megamart*****************

***********------------------**************

Counter Name:  Greater than 10 counter

television :  104.0

keyboard :  16.48

mouse :  14.49

Total: 134.97

***********------------------**************

***************PAID************************

In this class, we had different method definitions for goto_electronics_counter and a new test_electronics method.

Having defined the sequences, let’s proceed further to look at the common functionalities of each of these counters.

Identifying the common functionalities

In this section, let’s look at a graphical representation that shows the list of functions to be performed at each counter and the common functionalities between all four of them as follows. The common functionalities are highlighted in bold font in the following figure:

Figure 8.5 – Common operations performed across each counter

Figure 8.5 – Common operations performed across each counter

From Figure 8.5, all the functions highlighted in the bold font are common across all four counters. The review_items function is common across the less than 10 items counter, greater than 10 items counter, and electronics counter. The count_items function is common across the less than 10 items counter and greater than 10 items counter. The apply_coupon function is common across the greater than 10 items counter and the electronics counter. Since there are common functions or operations performed across all of the counters, we can look at creating a common way of designing them, too. This is where we can introduce the concept of templates.

Designing templates

As the name suggests, templates define a common template or format in which we can design an algorithmic flow of operations and reuse them when similar kinds of activities are performed. A template is one of the methods of design patterns in Python and can be used effectively while developing frameworks or libraries. Templates emphasize the concept of reusability in programming.

In this section, we will look at creating a class that handles all the common functions of all four counters discussed throughout this chapter, and create a method that handles the template that sequences or pipelines the steps to be executed in all the counters:

  1. To begin with, let’s create an abstract class named CommonCounter, and initialize the class with all the variables that will be used across all four counters. Refer to the following code:

    from abc import ABC, abstractmethod

    class CommonCounter(ABC):

        def __init__(self,items,name,scan,units,tax,item_type = None, weights = None, status = None):

            self.items = items

            self.name = name

            self.scan = scan

            self.units = units

            self.tax = tax

            self.item_type = item_type

            self.weights = weights

            self.status = status

  2. Next, we will be defining the return_cart, goto_counter, and scan_bar_code methods to take the input variables that are initialized in the class:

        def return_cart(self):

            cart_items = []

            for i in self.items:

                cart_items.append(i)

            return cart_items

        def goto_counter(self):

            countername = self.name

            return countername

        def scan_bar_code(self):

            codes = []

            for i in self.scan:

                codes.append(i)

            return codes

  3. Then, we will be defining the add_billing, add_tax, and calc_bill methods to take the input variables that are initialized in the class:

    def add_billing(self):

            self.codes = self.scan_bar_code()

            pricetag = []

            for i in self.units:

                pricetag.append(i)

            bill = dict(zip(self.codes, pricetag))

            return bill

         def add_tax(self):

            taxed = []

            for i in self.tax:

                taxed.append(i)

            return taxed

         def calc_bill(self):

            bill = self.add_billing()

            items = []

            cart_items = self.return_cart()

            calc_bill = []

            taxes = self.add_tax()

            for item,tax in zip(bill.items(),taxes):

                items.append(item[1])

                calc_bill.append(item[1] + item[1]*tax)

            finalbill = dict(zip(cart_items, calc_bill))

            return finalbill

  4. For simplicity, we will not be defining the print invoice method, and instead, will define the receive_payment method, which contains the definition of the print invoice method as well within the following code:

    def receive_payment(self):

            finalbill = self.calc_bill()

            final_total = sum(finalbill.values())

            print('**************ABC Megamart*****************')

            print('***********------------------**************')

            print('Counter Name: ', self.goto_counter())

            for item,price in finalbill.items():

                print(item,": ", price)

            print('Total:',final_total)

            print('***********------------------**************')

            print('***************PAID************************')

  5. Next, we will be defining the apply_coupon method, which returns a 0 value. This method can be redefined in the child classes if required:

    def apply_coupon(self):

            return 0

  6. In the preceding code snippets, we defined all the methods that are common across all four counters, whereas in the following code, we will be defining methods without statements so that they can be redefined within the child classes as and when required:

    def weigh_items(self):

            pass

    def add_price_tag(self):

            pass

    def count_items(self):

            pass

    def test_electronics(self):

            pass

  7. Then, let’s create review items as an abstract method that needs to have a definition within the child classes:

    @abstractmethod

        def review_items(self):

            pass

Now, the most important concept of templates is defined in the next code.

  1. Let’s define a method that handles the sequence of operations of a billing counter, and let’s use this method as a template for all the child classes that will be created for each billing counter:

    def pipeline_template(self):

            self.return_cart()

            self.goto_counter()

            self.review_items()

            self.count_items()

            self.test_electronics()

            self.weigh_items()

            self.add_price_tag()

            self.scan_bar_code()

            self.add_billing()

            self.add_tax()

            self.apply_coupon()

            self.calc_bill()

            self.receive_payment()

  2. We have defined the common class for all counters along with its template method, which can be reused for each individual billing counter.
  3. In the following code, we will create a child class for VegeCounter, with CommonCounter as a parent class:

    class VegeCounter(CommonCounter):

        def review_items(self):

            if ('Vegetables' in self.item_type):

                print("Move to Vege Counter")

            if ('Dairy' in self.item_type):

                print("Move to Vege Counter")

            if ('Fruits' in self.item_type):

                print("Move to Vege Counter")

        def weigh_items(self):

            item_weight = dict(zip(self.items, self.weights))

            return item_weight

        def add_price_tag(self):

            pricetag = []

            item_weight = self.weigh_items()

            for item,price in zip(item_weight.items(),self.units):

                pricetag.append(item[1]*price)

            return pricetag        

  4. In the preceding code, we have defined the review_items abstract method and we have also added statements in the definition of the weight_items and add_price_tag methods.
  5. Similarly, in the following code, let’s create a child class for ElectronicsCounter and define review_items (which is an abstract method), followed by redefining test_electronics (which did not have a definition in the CommonCounter base class):

    class ElectronicsCounter(CommonCounter):

        def review_items(self):

            if ('Electronics' in self.item_type):

                print("Move to Electronics Counter")

          def test_electronics(self):

            teststatus = []

            for i in self.status:

                teststatus.append(i)

            return teststatus

  6. Let’s now create a function to run the pipeline_template method for each of its child classes:

    def run_pipeline(counter = CommonCounter):

        counter.pipeline_template()

  7. Executing the run_pipeline method for each of the child classes results in the sequence of steps executed according to each billing counter. Let’s execute the pipeline method for the vegetable counter:

    run_pipeline(VegeCounter(items = ['onions', 'lettuce', 'apples', 'oranges'],

                             name = ['Vegetable Counter'],

                             scan = [113323,3434332,2131243,2332783],

                             units = [10,15,12,14],

                             tax = [0.04,0.03,0.035,0.025],

                             item_type = ['Vegetables'],

                             weights = [1,2,1.5,2.5]))

The output after running the pipeline for VegeCounter is as follows:

Move to Vege Counter

**************ABC Megamart*****************

***********------------------**************

Counter Name:  ['Vegetable Counter']

paperclips :  10.4

blue pens :  15.45

stapler :  12.42

pencils :  14.35

Total: 52.620000000000005

***********------------------**************

***************PAID************************

  1. Let’s now execute the pipeline method for ElectronicsCounter:

    run_pipeline(ElectronicsCounter(items = ['television','keyboard','mouse'],

                                    name = ['Electronics Counter'],

                                    scan = [113323,3434332,2131243],

                                    units = [100,16,14],

                                    tax = [0.04,0.03,0.035],

                                    item_type = ['Electronics'],

                                    status = ['pass','pass','pass']))

The output after running the pipeline for ElectronicsCounter is as follows:

Move to Electronics Counter

**************ABC Megamart*****************

***********------------------**************

Counter Name:  ['Electronics Counter']

television :  104.0

keyboard :  16.48

mouse :  14.49

Total: 134.97

***********------------------**************

***************PAID************************

In this section, we have created a template, but we have not repeated the same methods in multiple class definitions. The same CommonCounter abstract class can be reused for the definitions of the less than 10 items counter and the greater than 10 items counter as well. We learned how to create a template and implement template programming that emphasizes reusability in Python application development. We created a template that covers all the common functionalities across multiple sets of operations and reused the template multiple times.

Summary

In this chapter, we have learned the concepts of defining methods for a sequence of operations that follows an algorithm. We also defined classes that follow a sequence of operations from our core example. We created an abstract class that defines all the common functionalities of our core example, and we applied the templates design pattern to understand the concept of templates using the sequences from our core example.

Similar to other chapters covered in this book, this chapter also covered templates, which is a design pattern applied in metaprogramming to change the behavior of Python objects externally.

In the next chapter, we will be looking at the concept of abstract syntax trees with some interesting examples.

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

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