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:
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.
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.
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.
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:
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 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
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.
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
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.
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
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.
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
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 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:
Let’s begin with the vegetable counter.
The following are the steps for the operation of this counter:
class VegCounter():
def return_cart(self,*items):
cart_items = list(items)
return cart_items
def goto_vege_counter(self):
return 'Vegetables & Dairy'
def weigh_items(self,*weights,cart_items = None):
weight = list(weights)
item_weight = dict(zip(cart_items, weight))
return item_weight
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
def scan_bar_code(self,*scan):
codes = list(scan)
return codes
def add_billing(self,codes=None,pricetag=None):
self.codes = codes
self.pricetag = pricetag
bill = dict(zip(self.codes, self.pricetag))
return bill
def add_tax(self,*tax):
taxed = list(tax)
return taxed
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
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('***********------------------**************')
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************************')
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
***********------------------**************
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************************
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:
class LessThan10Counter():
…
def goto_less_t10_counter(self):
return 'Less than 10 counter'
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")
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")
…
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************************
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:
class GreaterThan10Counter():
…
def goto_greater_t10_counter(self):
return 'Greater than 10 counter'
…
def apply_coupon(self):
coupon_discount = 0.1
return coupon_discount
…
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.
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:
class ElectronicsCounter():
…
def goto_electronics_counter(self):
return 'Electronics counter'
def test_electronics(self,*status):
teststatus = list(status)
return teststatus
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.
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
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.
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:
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
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
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
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************************')
def apply_coupon(self):
return 0
def weigh_items(self):
pass
def add_price_tag(self):
pass
def count_items(self):
pass
def test_electronics(self):
pass
@abstractmethod
def review_items(self):
pass
Now, the most important concept of templates is defined in the next code.
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()
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
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
def run_pipeline(counter = CommonCounter):
counter.pipeline_template()
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************************
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.
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.