10. Object oriented programming

10.1. Pythonic style

In this chapter, we will see the basic differences between the OOPs methods in Python and other programming languages e.g. C++ and Java etc. This understanding is essential to use the Python language effectively; and to write the code in Pythonic ways, i.e. not converting a C++/Java code into a Python code.

Here we will see the actual logic behind various pieces of Python language e.g. instances, variables, method and @property etc. Also, we will see the combine usage of these pieces to complete a design with Agile methodology.

10.2. Simple data structure to Classes

In this section, we will convert our previous codes into classes.

10.2.1. Instance variable

First create a class, which can contain various columns of the csv file as shown in the below code. Please note the following points,

  • __init__ is the initializer (often called the constructor), which initializes the instance variables.
  • self.radius, self.date and self.metal are the instance variables which are created by the __init__.
# pythonic.py

import math

class Ring(object):
    """ Here we will see the actual logic behind various pieces of Python
    language e.g. instances, variables, method and @property etc.
    Also, we will see the combine usage of these pieces to complete a
    design with Agile methodology.
    """

    def __init__(self, date, metal, radius, price, quantity):
        """ init is not the constructor, but the initializer which
        initialize the instance variable

        self : is the instance

        __init__ takes the instance 'self' and populates it with the radius,
        metal, date etc. and store in a dictionary.

        self.radius, self.metal etc. are the instance variable which
        must be unique.
        """

        self.date = date
        self.metal = metal
        self.radius = radius
        self.price = price
        self.quantity = quantity

    def cost(self):
        return self.price * self.quantity

    def area(self):
        return math.pi * self.radius**2

10.2.2. Object of class

Let’s create an object ‘r’ of the class ‘Ring’, to verify the operation of the class, as shown below,

>>> from pythonic import Ring
>>> r = Ring("2017=08-10", "Gold", 5.5, 10.5, 10)
>>> r.metal
'Gold'
>>> r.cost()
105.0
>>> r.area()
95.03317777109125

10.2.3. Class variable and initialization

Lets, add some class variables to the above class. And define some initial values to the arguments in the __init__ function as shown below,

Note

Class variable should be used to create the shared variables. It is always good to put the shared variables on the class level rather than instance level.

# pythonic.py

import math
import time

class Ring(object):
    """ Here we will see the actual logic behind various pieces of Python
    language e.g. instances, variables, method and @property etc.
    Also, we will see the combine usage of these pieces to complete a
    design with Agile methodology.
    """

    # class variables
    date = time.strftime("%Y-%m-%d", time.gmtime()) # today's date "YYYY-mm-dd"
    center = 0.0 # center of the ring

    def __init__(self, date=date, metal="Copper", radius=5.0,
                price=5.0, quantity=5):
        """ init is not the constructor, but the initializer which
        initialize the instance variable

        self : is the instance

        __init__ takes the instance 'self' and populates it with the radius,
        metal, date etc. and store in a dictionary.

        self.radius, self.metal etc. are the instance variable which
        must be unique.
        """

        self.date = date
        self.metal = metal
        self.radius = radius
        self.price = price
        self.quantity = quantity

    def cost(self):
        return self.price * self.quantity

    def area(self):
        return math.pi * self.radius**2

Lets check the functionality of the above code,

$ python -i pythonic.py

Note that we used ‘-i’ option, therefore there is no need to import the class ‘Ring’ as shown in below code,

>>> r = Ring() # no paratmer pass therefore default values will be used
>>> r.date # instance variable
'2017-10-27'
>>> r.radius # instance variable
5.0
>>> r.center # class variable
0.0
>>> r.cost() # class method
25.0
>>> r.area() # class method
78.53981633974483

Also, we can modify the instance and class variables values as shown below,

>>> r.price # current value
5.0
>>> r.quantity
5
>>> r.cost()
25.0
>>> r.quantity=10 # modify instance variable
>>> r.cost() # price is changed to 50
50.0
>>> r.center = 10 # modify class variable
>>> r.center
10

Note

The value of the variables are stored in the dictionary, whose contents can be seen using ‘__dict__’, i.e.,

>>> r.__dict__
{'date': '2017-10-27', 'metal': 'Copper', 'radius': 5.0,
'price': 5.0, 'quantity': 10, 'center': 10}

10.3. Shipping product and Agile methodology

In previous section, we create the object of the class in the Python shell. Now, we will create object of the class in the file itself, as show below,

Important

Note that, at this moment, we did not add to many features to class in Listing 10.1. We added only two methods here i.e. ‘area’ and ‘cost’. Now, we are ready to ship this product to our customers.

We will add more features according to feedback provided by the costumers as shown in subsequent sections. This is called the ‘Agile’ methodology. This will give us a chance to understand the psychology behind the several elements of the Python language.

Listing 10.1 Shipping product is ready
# pythonic.py

import math
import time

class Ring(object):
    """ Here we will see the actual logic behind various pieces of Python
    language e.g. instances, variables, method and @property etc.
    Also, we will see the combine usage of these pieces to complete a
    design with Agile methodology.
    """

    # class variables
    date = time.strftime("%Y-%m-%d", time.gmtime()) # today's date "YYYY-mm-dd"
    center = 0.0 # center of the ring

    def __init__(self, date=date, metal="Copper", radius=5.0,
                price=5.0, quantity=5):
        """ init is not the constructor, but the initializer which
        initialize the instance variable

        self : is the instance

        __init__ takes the instance 'self' and populates it with the radius,
        metal, date etc. and store in a dictionary.

        self.radius, self.metal etc. are the instance variable which
        must be unique.
        """

        self.date = date
        self.metal = metal
        self.radius = radius
        self.price = price
        self.quantity = quantity

    def cost(self):
        return self.price * self.quantity

    def area(self):
        return math.pi * self.radius**2

def main():
    print("Center of the Ring is at:", Ring.center) # modify class variable
    r = Ring(price=8) # modify only price
    print("Radius:{0}, Cost:{1}".format(r.radius, r.cost()))

if __name__ == '__main__':
    main()

Following are the results for above code,

$ python pythonic.py
Center of the Ring is at: 0.0
Radius:5.0, Cost:40

10.4. Attribute access

Before moving further, let us understand the attribute access (i.e. variable in the class).

10.4.1. get, set and del

In python, everything is an object. And there are only three operations which can be applied to the objects i.e.,

  • get
  • set
  • delete

These operations are used as below,

>>> from pythonic import Ring
>>> r = Ring()
>>>
>>> r.metal # get operation
'Copper'
>>>
>>> r.metal = "Gold" # set operation
>>> r.metal
'Gold'
>>>
>>> del r.metal, r.date, r.price  # delete operation
>>> r.metal
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Ring' object has no attribute 'metal'
>>>
>>> r.__dict__  # only radius and quantity left
{'radius': 5.0, 'quantity': 5}

Warning

If attribute does not exist in the dictionary, then it will be created, e.g. in the below code, we used h.metals (plural metals) for the metal name instead of h.metal, therefore a new attribute will be created. Hence, be careful while setting the attributes of the class.

>>> r.metals = "Iron" # add item to dict
>>> r.__dict__
{'radius': 5.0, 'quantity': 5, 'metals': 'Iron'}

Note

The class method has two layers of the get-set-del operations as shown below,

>>> r.area()
78.53981633974483
>>>
>>> (r.area)() # (layer1)(layer2)
78.53981633974483
>>>
>>> a = r.area # layer 1
>>> a
<bound method Ring.area of <pythonic.Ring object at 0xb7132f2c>>
>>> a() # layer 2
78.53981633974483

10.4.2. getattr, setattr and delattr

getattr, setattr and delattr are invoked when we use the get, set and delete operations respectively. The knowledge of these methods can be very useful in writing the general purpose codes as shown in Section 10.5.2.

Below an example of getattr and setattr, where the columns are printed using ‘getattr’. Note that the code is generalized here, as we can print all the columns using one statement only i.e. Lines 10-11.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
>>> from pythonic import Ring
>>> r = Ring()
>>> getattr(r, 'metal')  # get
'Copper'
>>> setattr(r, 'metal', 'Gold')  # set
>>> r.metal
'Gold'
>>>
>>> out_col = ['metal', 'radius', 'quantity']
>>> for col in out_col: # print columns and value using getattr
...     print("{0} : {1}".format(col, getattr(r, col)))
...
metal : Gold
radius : 5.0
quantity : 5
>>>
>>> delattr(r, 'metal') # delete 'metal'
>>> r.__dict__ # 'metal' is removed from dictionary
{'date': '2017-10-27', 'radius': 5.0, 'price': 5.0, 'quantity': 5}

10.5. Users

Lets assume that we have two types of users for our product. One of them is ‘mathematician’ who is using our product for the mathematical analysis of the data; whereas the other is a ‘contributer’, who is implementing additional features to the product. Both of them will provide some feedbacks according to their need and we will modify our class to meet their requirement.

10.5.1. Average area

The first user, i.e. mathematician, uses our design to calculate the average area of the ring, as shown below,

Listing 10.2 Average area
# mathematician.py
# user-1: using Ring.py for mathematical analysis

from random import random, seed
from pythonic import Ring

def calc_avg_area(n=5, seed_value=3):
    # seed for random number generator
    seed(seed_value)

    # random radius
    rings = [Ring(radius=random()) for i in range(n)]

    total = 0
    for r in rings:
        total += r.area()
        # # print values for each iteration
        print("%0.2f, %0.2f, %0.2f" % (r.radius, r.area(), total))
    avg_area = sum([r.area() for r in rings])/n

    return avg_area

def main():
    # generate 'n' rings
    n = 10
    avg_area = calc_avg_area(n=10)
    print("\nAverage area for n={0} is n/{0} = {1:.2f}".format(n, avg_area))

if __name__ == '__main__':
    main()

Following is the result of above code,

$ python mathematician.py

0.24, 0.18, 0.18
0.54, 0.93, 1.11
0.37, 0.43, 1.54
0.60, 1.15, 2.68
0.63, 1.23, 3.91
0.07, 0.01, 3.93
0.01, 0.00, 3.93
0.84, 2.20, 6.13
0.26, 0.21, 6.34
0.23, 0.17, 6.52

Average area for n=10 is n/10 = 0.65

10.5.2. Table formatting

The second user, i.e. contributor, has added the table-formatting functionality for class ‘Ring’.

Note

The code is generalized format as compared to Listing 9.1. Following are the changes made here,

  • At line 31, the conversion is generalized using ‘list comprehension’.
  • The function ‘print_table (Lines 67-76)’ is generalized using ‘getattr’ method. Here, we can print desired columns of the table by passing a list of ‘column names’.
Listing 10.3 Table formatter
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# contributor.py
# user-2: additional features will be added to pythonic.py by the contributor

import csv

from pythonic import Ring

# this code is copied from datamine.py file and modified slightly
# to include the type of the data
def read_file(filename, types, mode='warn'):
    ''' read csv file and save data in the list '''

    # check for correct mode
    if mode not in ['warn', 'silent', 'stop']:
        raise ValueError("possible modes are 'warn', 'silent', 'stop'")

    ring_data = [] # create empty list to save data

    with open (filename, 'r') as f:
        rows =  csv.reader(f)
        header = next(rows) # skip the header

        # change the types of the columns
        for row in rows:
            try:
                # row[2] = float(row[2]) # radius
                # row[3] = float(row[3]) # price
                # row[4] = int(row[4]) # quantity

                # generalized conversion
                row = [d_type(val) for d_type, val in zip(types, row)]
            except ValueError as err: # process value error only
                if mode == 'warn':
                    print("Invalid data, row is skipped")
                    print('Row: {}, Reason : {}'.format(row_num, err))
                elif mode == 'silent':
                    pass # do nothing
                elif mode == 'stop':
                    raise # raise the exception
                continue

            # ring_data.append(tuple(row))

            # append data in list in the form of tuple
            # row_dict = {
                    # 'date' : row[0],
                    # 'metal' : row[1],
                    # 'radius' : row[2],
                    # 'price' : row[3],
                    # 'quantity' : row[4]
                # }

            # row_dict = Ring(row[0], row[1], row[2], row[3], row[4])
            # # use below or above line
            row_dict = Ring(
                    date = row[0],
                    metal = row[1],
                    radius = row[2],
                    price = row[3],
                    quantity = row[4]
                )

            ring_data.append(row_dict)

    return ring_data

# table formatter
def print_table(list_name, col_name=['metal', 'radius']):
    """ print the formatted output """

    for c in col_name: # print header
        print("{:>7s}".format(c), end=' ')
    print() # print empty line
    for l in list_name: # print values
        for c in col_name:
            print("{:>7s}".format(str(getattr(l, c))), end=' ')
        print()

def main():
    # list correct types of columns in csv file
    types =[str, str, float, float, int]

    # read file and save data in list
    list_data = read_file('price.csv', types)

    # formatted output
    print_table(list_data)
    print()
    print_table(list_data, ['metal', 'radius', 'price'])

if __name__ == '__main__':
    main()

Following are the output for the above code,

$ python contributor.py

metal  radius
  Gold     5.5
Silver    40.3
  Iron     9.2
  Gold     8.0
Copper     4.1
  Iron    3.25

 metal  radius   price
  Gold     5.5   80.99
Silver    40.3     5.5
  Iron     9.2   14.29
  Gold     8.0   120.3
Copper     4.1   70.25
  Iron    3.25   10.99

10.6. Requirement : perimeter method

In Listing 10.2, the mathematician calculated the average area. Suppose, mathematician want to do some analysis on the perimeter of the ring as well, therefore he asked us to add a method to calculate the perimeter as well.

This is quite easy task, and can be implemented as below,

# pythonic.py

import math
import time

class Ring(object):
    """ Here we will see the actual logic behind various pieces of Python
    language e.g. instances, variables, method and @property etc.
    Also, we will see the combine usage of these pieces to complete a
    design with Agile methodology.
    """

    # class variables
    date = time.strftime("%Y-%m-%d", time.gmtime()) # today's date "YYYY-mm-dd"
    center = 0.0 # center of the ring

    def __init__(self, date=date, metal="Copper", radius=5.0,
                price=5.0, quantity=5):
        """ init is not the constructor, but the initializer which
        initialize the instance variable

        self : is the instance

        __init__ takes the instance 'self' and populates it with the radius,
        metal, date etc. and store in a dictionary.

        self.radius, self.metal etc. are the instance variable which
        must be unique.
        """

        self.date = date
        self.metal = metal
        self.radius = radius
        self.price = price
        self.quantity = quantity

    def cost(self):
        return self.price * self.quantity

    def area(self):
        return math.pi * self.radius**2

    def perimeter(self):
        return 2 * math.pi * self.radius

def main():
    print("Center of the Ring is at:", Ring.center) # modify class variable
    r = Ring(price=8) # modify only price
    print("Radius:{0}, Cost:{1}".format(r.radius, r.cost()))
    print("Radius:{0}, Perimeter:{1:0.2f}".format(r.radius, r.perimeter()))

if __name__ == '__main__':
    main()

10.7. No data hiding in Python

In previous section, we added the method ‘perimeter’ in the design. Note that, in Python all the attributes can be directly accessed by the clients, and there is no concept of data hiding in the Python. Let’s understand this by an example.

Important

In the below code, the mathematician uses the attribute ‘radius’ directly by changing it to ‘r.radius = expansion(r.radius)’ at Line 39. Then the new radius is used to calculate the perimeter at Line 40. In the other word, value of radius is changed in the dictionary itself, and then the method perimeter() is used for calculation.

Such access to attributes is available in Python only; whereas other languages e.g. Java and C++ etc. uses the concept of data hiding (using private and protected variable) and the attributes can not be directly accessed. These languages provide some get and set method to access and modify the attributes, i.e. we can make a local copy of the variable for further calculation; whereas in Python, the value is changed in the dictionary itself.

Data hiding is required in C++ and Java etc. as direct access can be a serious problem there and can not be resolved. In Python, data is not hidden from user and we have various methods to handle all kind of situations as shown in this chapter.

Listing 10.4 Accessing the class attribute directly
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# mathematician.py
# user-1: using Ring.py for mathematical analysis

from random import random, seed
from pythonic import Ring

def calc_avg_area(n=5, seed_value=3):
    # seed for random number generator
    seed(seed_value)

    # random radius
    rings = [Ring(radius=random()) for i in range(n)]

    total = 0
    for r in rings:
        total += r.area()
        # # print values for each iteration
        print("%0.2f, %0.2f, %0.2f" % (r.radius, r.area(), total))
    avg_area = sum([r.area() for r in rings])/n

    return avg_area

def expansion(radius=1.0, expansion=2.0):
        radius *= expansion  # 2.0 times radius expansion due to heat
        return radius

def main():
    # # generate 'n' rings
    # n = 10
    # avg_area = calc_avg_area(n=10)
    # print("\nAverage area for n={0} is n/{0} = {1:.2f}".format(n, avg_area))

    radii = [1, 3, 5] # list of radius
    rings = [Ring(radius=r) for r in radii] # create object of different radius
    for r in rings:
        print("Radius:", r.radius)
        print("Perimeter at room temperature: %0.2f" % r.perimeter())
        # radius after expansion
        r.radius = expansion(r.radius) # modifying the attribute of the class
        print("Perimeter after heating:, %0.2f" % r.perimeter())

if __name__ == '__main__':
    main()

Following is the output of above code,

$ python mathematician.py

Radius: 1
Perimeter at room temperature: 6.28
Perimeter after heating:, 12.57

Radius: 3
Perimeter at room temperature: 18.85
Perimeter after heating:, 37.70

Radius: 5
Perimeter at room temperature: 31.42
Perimeter after heating:, 62.83

10.8. Inheritance overview

Lets review the inheritance in Python quickly. Then we will see some good usage of inheritance in this chapter.

Note

No prior knowledge of the ‘super()’ and ‘multiple inheritance’ is required. In this chapter, we will see various examples to understand these topics.

  • First create a parent class “Animal” as below,
>>> class Animal(object):
...     def __init__(self, name):
...             self.name = name
...
...     def sound(self):
...             print("Loud or soft sound")
...
...     def wild(self):
...             print("Wild or pet animail")
>>>
>>> a = Animal("Tiger")
>>> a.sound()
Loud or soft sound
  • Now, create child class “PetAnimal” which inherit the class “Animal”,
>>> class PetAnimal(Animal):
...     def pet_size(self):
...             print("small or big")
...
>>> p  = PetAnimal("cat")
>>> p.pet_size()
small or big
>>> p.sound()  # inherit from Animal
Loud or soft sound
  • Next, overide the method of class “Animal” in the child class “Dog”,
>>> class Dog(Animal):
...     def sound(self):
...             print("Dog barks")
...
>>> d = Dog("Tommy")
>>> d.sound() # override the 'sound' of class Animal
Dog barks
  • We can use both parent and child class method with same name. This can be done using ‘super()’ as below,
>>> class Tiger(Animal):
...     def sound(self):
...             print("Tiger roars")
...             super().sound()  # call the parent class 'sound'
>>> t = Tiger("Tigger")
>>> t.sound() # invoke 'sound' of both child and parent
Tiger roars
Loud or soft sound
  • Multiple inheritance is possible in Python, which can be used to handle complex situations. Below is an example of multiple inheritance,
>>> class Cat(Tiger, Animal):
...     pass
...
>>> c = Cat("Kitty")
>>> c.sound()
Tiger roars
Loud or soft sound
>>> c.wild()
Wild or pet animail
  • Note that, Python creates a MRO (method resolution order) for multiple inheritance, and if it can not be created then error will be reported. The MRO for class ‘Cat’ is shown below,
>>> help(cat)
    class Cat(Tiger, Animal)
     |  Method resolution order:
     |      Cat
     |      Tiger
     |      Animal
  • If we inherit Animal first and next Tiger, then below error will occur; because Python uses ‘child first’ approach and this inheritance will call the Parent first i.e. MRO will be “Cat->Animal->Tiger”. And it will report error as it has ‘parent first’ i.e. Animal comes before Tiger.
>>> class Rat(Animal, Tiger):
...     pass
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Cannot create a consistent method resolution
order (MRO) for bases Animal, Tiger

10.9. New user : making boxes

Suppose, we get another user for our class Ring (i.e. file pythonic.py), who is creating boxes for the ring. For he needs slightly greater perimeter of the box than the ring.

Note that this user is inheriting the class ‘Ring’, and overriding the method ‘perimeter()’ at Line 11 as shown below,

Listing 10.5 Box company modifying the class method ‘perimeter()’
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# box.py
# user-3 : creating boxes for the ring

from pythonic import Ring

class Box(Ring):
    """ Modified perimeter for creating the box """

    def perimeter(self): # override the method 'perimeter'
        # perimeter is increased 2.0 times
        return Ring.perimeter(self) * 2.0

def main():
    b = Box(radius=8) # pass radius = 8
    print("Radius:", b.radius)
    print("Modified perimeter: %0.2f" % b.perimeter()) # (2*pi*radius) * 2

if __name__ == '__main__':
    main()

Following is the output of above code,

$ python box.py
Radius: 8
Modified perimeter: 100.53

Note

Now, we have two users who are modifying the class attributes. First is the mathematician (mathematician.py), who is modifying the ‘radius’ in Listing 10.4. And other is the box company, who is modifying the method ‘perimeter’ in Listing 10.5. Lastly, we have a contributor who is creating a ‘table formatter’ in Listing 10.3 for displaying the output nicely.

10.10. Rewrite table formatter using Inheritance

10.10.1. Abstract class

The contributor decided to add more print-formats for the table. For this, he decided to use inheritance to rewrite the code Listing 10.3 using inheritance.

Also, the contributor used the abstract class at Line 11 with two abstract methods. The abstract methods are compulsory to be implemented in the child class.

Important

  • The abstract methods are compulsory to be implemented in the child class. In the other words, the abstract class is the way to force the child class to implement certain method, which are decorated with ‘abstractmethod’
  • Here the aim is to rewrite the code in Listing 10.3 such that we need not to change anything in the function ‘main()’. In the other words, if the main() function works fine as it is, then it ensures that the code will not break for the users, who are using Listing 10.3 for printing the table.

Below is the modified Listing 10.3,

Listing 10.6 Print-format using inheritance
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
 # contributor.py
 # user-2: additional features will be added to pythonic.py by the contributor

 from abc import ABC, abstractmethod
 import csv

 from pythonic import Ring


 # Abstract class for table-format
 class TableFormat(object):
     """ Abastract class """

     @abstractmethod
     def heading(self, header): # must be implemented in child class
         pass

     @abstractmethod
     def row(self, row_data): # must be implemented in child class
         pass


 # text format for table
 class TextFormat(TableFormat):
     """ Text format for table """

     def heading(self, header): # print headers
         for h in header:
             print("{:>7s}".format(h), end=' ')
         print()

     def row(self, row_data): # print rows
         for r in row_data:
             print("{:>7s}".format(r), end=' ')
         print()


 # this code is copied from datamine.py file and modified slightly
 # to include the type of the data
 def read_file(filename, types, mode='warn'):
     ''' read csv file and save data in the list '''

     # check for correct mode
     if mode not in ['warn', 'silent', 'stop']:
         raise ValueError("possible modes are 'warn', 'silent', 'stop'")

     ring_data = [] # create empty list to save data

     with open (filename, 'r') as f:
         rows =  csv.reader(f)
         header = next(rows) # skip the header

         # change the types of the columns
         for row in rows:
             try:
                 # row[2] = float(row[2]) # radius
                 # row[3] = float(row[3]) # price
                 # row[4] = int(row[4]) # quantity

                 # generalized conversion
                 row = [d_type(val) for d_type, val in zip(types, row)]
             except ValueError as err: # process value error only
                 if mode == 'warn':
                     print("Invalid data, row is skipped")
                     print('Row: {}, Reason : {}'.format(row_num, err))
                 elif mode == 'silent':
                     pass # do nothing
                 elif mode == 'stop':
                     raise # raise the exception
                 continue

             # ring_data.append(tuple(row))

             # append data in list in the form of tuple
             # row_dict = {
                     # 'date' : row[0],
                     # 'metal' : row[1],
                     # 'radius' : row[2],
                     # 'price' : row[3],
                     # 'quantity' : row[4]
                 # }

             # row_dict = Ring(row[0], row[1], row[2], row[3], row[4])
             # # use below or above line
             row_dict = Ring(
                     date = row[0],
                     metal = row[1],
                     radius = row[2],
                     price = row[3],
                     quantity = row[4]
                 )

             ring_data.append(row_dict)

     return ring_data


 # table formatter
 def print_table(list_name, col_name=['metal', 'radius'],
         out_format=TextFormat()): # note that class is passed here
     """ print the formatted output """

     # for c in col_name: # print header
         # print("{:>7s}".format(c), end=' ')
     # print() # print empty line
     # for l in list_name: # print values
         # for c in col_name:
             # print("{:>7s}".format(str(getattr(l, c))), end=' ')
         # print()

     # invoke class-method for printing heading
     out_format.heading(col_name) # class is passed to out_format
     for l in list_name:
         # store row in a list
         row_data = [str(getattr(l, c)) for c in col_name]
         out_format.row(row_data) # pass rows to class-method row()


 def main():
     # list correct types of columns in csv file
     types =[str, str, float, float, int]

     # read file and save data in list
     list_data = read_file('price.csv', types)

     # formatted output
     print_table(list_data)
     print()
     print_table(list_data, ['metal', 'radius', 'price'])

 if __name__ == '__main__':
     main()

Now run the code to see whether it is working as previously or not. The below output is same as for Listing 10.3,

$ python contributor.py

  metal  radius
   Gold     5.5
 Silver    40.3
   Iron     9.2
   Gold     8.0
 Copper     4.1
   Iron    3.25

  metal  radius   price
   Gold     5.5   80.99
 Silver    40.3     5.5
   Iron     9.2   14.29
   Gold     8.0   120.3
 Copper     4.1   70.25
   Iron    3.25   10.99

10.10.2. csv format

Since the above code is working fine, therefore the contributor can add as many format as possible, just by adding a new class. In the below code, he added a ‘csv format’ for the printing,

Listing 10.7 csv format is added
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# contributor.py
# user-2: additional features will be added to pythonic.py by the contributor

from abc import ABC, abstractmethod
import csv

from pythonic import Ring


# Abstract class for table-format
class TableFormat(object):
    """ Abastract class """

    @abstractmethod
    def heading(self, header): # must be implemented in child class
        pass

    @abstractmethod
    def row(self, row_data): # must be implemented in child class
        pass


# text format for table
class TextFormat(TableFormat):
    """ Text format for table """

    def heading(self, header): # print headers
        for h in header:
            print("{:>7s}".format(h), end=' ')
        print()

    def row(self, row_data): # print rows
        for r in row_data:
            print("{:>7s}".format(r), end=' ')
        print()


# csv format for table
class CSVFormat(TableFormat):
    """ Text format for table """

    def heading(self, header): # print headers
        print(','.join(header))

    def row(self, row_data): # print rows
        print(",".join(row_data))

# this code is copied from datamine.py file and modified slightly
# to include the type of the data
def read_file(filename, types, mode='warn'):
    ''' read csv file and save data in the list '''

    # check for correct mode
    if mode not in ['warn', 'silent', 'stop']:
        raise ValueError("possible modes are 'warn', 'silent', 'stop'")

    ring_data = [] # create empty list to save data

    with open (filename, 'r') as f:
        rows =  csv.reader(f)
        header = next(rows) # skip the header

        # change the types of the columns
        for row in rows:
            try:
                # row[2] = float(row[2]) # radius
                # row[3] = float(row[3]) # price
                # row[4] = int(row[4]) # quantity

                # generalized conversion
                row = [d_type(val) for d_type, val in zip(types, row)]
            except ValueError as err: # process value error only
                if mode == 'warn':
                    print("Invalid data, row is skipped")
                    print('Row: {}, Reason : {}'.format(row_num, err))
                elif mode == 'silent':
                    pass # do nothing
                elif mode == 'stop':
                    raise # raise the exception
                continue

            # ring_data.append(tuple(row))

            # append data in list in the form of tuple
            # row_dict = {
                    # 'date' : row[0],
                    # 'metal' : row[1],
                    # 'radius' : row[2],
                    # 'price' : row[3],
                    # 'quantity' : row[4]
                # }

            # row_dict = Ring(row[0], row[1], row[2], row[3], row[4])
            # # use below or above line
            row_dict = Ring(
                    date = row[0],
                    metal = row[1],
                    radius = row[2],
                    price = row[3],
                    quantity = row[4]
                )

            ring_data.append(row_dict)

    return ring_data


# table formatter
def print_table(list_name, col_name=['metal', 'radius'],
        out_format=TextFormat()): # note that class is passed here
    """ print the formatted output """

    # for c in col_name: # print header
        # print("{:>7s}".format(c), end=' ')
    # print() # print empty line
    # for l in list_name: # print values
        # for c in col_name:
            # print("{:>7s}".format(str(getattr(l, c))), end=' ')
        # print()

    # invoke class-method for printing heading
    out_format.heading(col_name) # class is passed to out_format
    for l in list_name:
        # store row in a list
        row_data = [str(getattr(l, c)) for c in col_name]
        out_format.row(row_data) # pass rows to class-method row()


def main():
    # list correct types of columns in csv file
    types =[str, str, float, float, int]

    # read file and save data in list
    list_data = read_file('price.csv', types)

    # # formatted output
    # print_table(list_data)
    # print()
    print_table(list_data, ['metal', 'radius', 'price'], out_format=TextFormat())

    print()
    print_table(list_data, ['metal', 'radius', 'price'], out_format=CSVFormat())


if __name__ == '__main__':
    main()

Following is the output of above listing,

$ python contributor.py
  metal  radius   price
   Gold     5.5   80.99
 Silver    40.3     5.5
   Iron     9.2   14.29
   Gold     8.0   120.3
 Copper     4.1   70.25
   Iron    3.25   10.99

metal,radius,price
Gold,5.5,80.99
Silver,40.3,5.5
Iron,9.2,14.29
Gold,8.0,120.3
Copper,4.1,70.25
Iron,3.25,10.99

10.11. Advance inheritance

In this section, __init__ function of parent class is inherited by the child class. Also, a good usage of ‘multiple inheritance’ and ‘class with one method’ is shown.

10.11.1. Printing outputs to files

In Listing 10.7, the output can be printed on the screen only. There we requested the contributor, that it will be better if we can save the output in the files as well.

To add this feature, the contributor decided to add an __init__ function to add the print functionality. It is a good approach otherwise we need to add the ‘output-file’ logic in each format-class.

Below is the code for saving the data in file. Following are the functionality added to this design,

  • Outputs can be saved in the files.
  • CSVFormat gets this ability through inheriting the parent class __init__.
  • TextFormat override the parent class __init__ to increase/decrease the width. Also, it uses ‘super()’ to inherit the parent class __init__ so that the output can be saved in the file.
Listing 10.8 Print data in file and screen
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# contributor.py
# user-2: additional features will be added to pythonic.py by the contributor

import sys
import csv
from abc import ABC, abstractmethod

from pythonic import Ring


# Abstract class for table-format
class TableFormat(object):
    """ Abastract class """

    # print data to file or screen
    def __init__(self, out_file=None):
        if out_file == None:
            # stdout is the location where python prints the output
            out_file = sys.stdout
        self.out_file = out_file

    @abstractmethod
    def heading(self, header): # must be implemented in child class
        pass

    @abstractmethod
    def row(self, row_data): # must be implemented in child class
        pass


# text format for table
class TextFormat(TableFormat):
    """ Text format for table """

    # option for modifying width
    def __init__(self, width=7, out_file=None): # override init
        # inherit parent init as well to save data in file
        super().__init__(out_file)
        self.width = width

    def heading(self, header): # print headers
        for h in header:
            print("{0:>{1}s}".format(h, self.width),
                    end=' ', file=self.out_file)
        print(file=self.out_file)

    def row(self, row_data): # print rows
        for r in row_data:
            print("{0:>{1}s}".format(r, self.width),
                    end=' ', file=self.out_file)
        print(file=self.out_file)


# csv format for table
class CSVFormat(TableFormat):
    """ Text format for table """

    # init will be inherited from parent to save data in file

    def heading(self, header): # print headers
        print(','.join(header), file=self.out_file)

    def row(self, row_data): # print rows
        print(",".join(row_data), file=self.out_file)

# this code is copied from datamine.py file and modified slightly
# to include the type of the data
def read_file(filename, types, mode='warn'):
    ''' read csv file and save data in the list '''

    # check for correct mode
    if mode not in ['warn', 'silent', 'stop']:
        raise ValueError("possible modes are 'warn', 'silent', 'stop'")

    ring_data = [] # create empty list to save data

    with open (filename, 'r') as f:
        rows =  csv.reader(f)
        header = next(rows) # skip the header

        # change the types of the columns
        for row in rows:
            try:
                # row[2] = float(row[2]) # radius
                # row[3] = float(row[3]) # price
                # row[4] = int(row[4]) # quantity

                # generalized conversion
                row = [d_type(val) for d_type, val in zip(types, row)]
            except ValueError as err: # process value error only
                if mode == 'warn':
                    print("Invalid data, row is skipped")
                    print('Row: {}, Reason : {}'.format(row_num, err))
                elif mode == 'silent':
                    pass # do nothing
                elif mode == 'stop':
                    raise # raise the exception
                continue

            # ring_data.append(tuple(row))

            # append data in list in the form of tuple
            # row_dict = {
                    # 'date' : row[0],
                    # 'metal' : row[1],
                    # 'radius' : row[2],
                    # 'price' : row[3],
                    # 'quantity' : row[4]
                # }

            # row_dict = Ring(row[0], row[1], row[2], row[3], row[4])
            # # use below or above line
            row_dict = Ring(
                    date = row[0],
                    metal = row[1],
                    radius = row[2],
                    price = row[3],
                    quantity = row[4]
                )

            ring_data.append(row_dict)

    return ring_data


# table formatter
def print_table(list_name, col_name=['metal', 'radius'],
        out_format=TextFormat(out_file=None)): # note that class is passed here
    """ print the formatted output """

    # for c in col_name: # print header
        # print("{:>7s}".format(c), end=' ')
    # print() # print empty line
    # for l in list_name: # print values
        # for c in col_name:
            # print("{:>7s}".format(str(getattr(l, c))), end=' ')
        # print()

    # invoke class-method for printing heading
    out_format.heading(col_name) # class is passed to out_format
    for l in list_name:
        # store row in a list
        row_data = [str(getattr(l, c)) for c in col_name]
        out_format.row(row_data) # pass rows to class-method row()


def main():
    # list correct types of columns in csv file
    types =[str, str, float, float, int]

    # read file and save data in list
    list_data = read_file('price.csv', types)

    # # formatted output
    # print_table(list_data)
    # print()
    print_table(list_data, ['metal', 'radius', 'price'],
            out_format=TextFormat())

    # print in file
    out_file = open("text_format.txt", "w") # open file in write mode
    print_table(list_data, ['metal', 'radius', 'price'],
            out_format=TextFormat(out_file=out_file))
    out_file.close()

    # print in file with width = 15
    out_file = open("wide_text_format.txt", "w") # open file in write mode
    print_table(list_data, ['metal', 'radius', 'price'],
            out_format=TextFormat(width=15, out_file=out_file))
    out_file.close()

    print()
    print_table(list_data, ['metal', 'radius', 'price'],
            out_format=CSVFormat())
    # print in file
    out_file = open("csv_format.csv", "w") # open file in write mode
    print_table(list_data, ['metal', 'radius', 'price'],
            out_format=CSVFormat(out_file=out_file))
    out_file.close()


if __name__ == '__main__':
    main()

Following are the outputs of the above listing.

Note

The previous main function is also working fine. This check is very important to confirm that the codes of the exitsting user will not bread due to updating of the design.

$ python contributor.py

  metal  radius   price
   Gold     5.5   80.99
 Silver    40.3     5.5
   Iron     9.2   14.29
   Gold     8.0   120.3
 Copper     4.1   70.25
   Iron    3.25   10.99

metal,radius,price
Gold,5.5,80.99
Silver,40.3,5.5
Iron,9.2,14.29
Gold,8.0,120.3
Copper,4.1,70.25
Iron,3.25,10.99

Below are the contents of the file generated by above listing,

$ cat text_format.txt

 metal  radius   price
  Gold     5.5   80.99
Silver    40.3     5.5
  Iron     9.2   14.29
  Gold     8.0   120.3
Copper     4.1   70.25
  Iron    3.25   10.99
$ cat wide_text_format.txt

 metal          radius           price
  Gold             5.5           80.99
Silver            40.3             5.5
  Iron             9.2           14.29
  Gold             8.0           120.3
Copper             4.1           70.25
  Iron            3.25           10.99
$ cat csv_format.csv
metal,radius,price
Gold,5.5,80.99
Silver,40.3,5.5
Iron,9.2,14.29
Gold,8.0,120.3
Copper,4.1,70.25
Iron,3.25,10.99

10.11.2. Mixin : multiple inheritance

10.11.3. Put quotes around data

Suppose, we want to put quotes around all the data in printed output. This can be accomplished as below,

>>> from contributor import *
>>> class QuoteData(TextFormat):
...     def row(self, row_data):
...             # put quotes around data
...             quoted_data = ['"{0}"'.format(r) for r in row_data]
...             super().row(quoted_data)
...
>>> types =[str, str, float, float, int]
>>> list_data = read_file('price.csv', types)
>>> print_table(list_data, ['metal', 'radius', 'price'], out_format=QuoteData())
  metal  radius   price
 "Gold"   "5.5" "80.99"
"Silver"  "40.3"   "5.5"
 "Iron"   "9.2" "14.29"
 "Gold"   "8.0" "120.3"
"Copper"   "4.1" "70.25"
 "Iron"  "3.25" "10.99"

Note

The problem with above method is that it is applicable to class ‘TextFormat’ only (not to CSVFormat). This can be generalized using Mixin as shown below,

10.11.4. Put quotes using Mixin

We can use mix two class as shown below,

Important

Please note following points in the below code,

  • In Python, inheritance follows two rule

    • Child is checked before Parent.
    • Parents are checked in order.
  • The ‘super()’ is used in he class “QuoteData”, which will call the __init__ function. But this class does not inherit from any other class (or inherits from Python-object class).

  • The __init__ function function for the class “QuoteData” will be decided by the child class, i.e. the class MixinQuoteCSV is inheriting “QuoteData” and “CSVFormat”.

  • Since, the parents are checked in order, therefore “QuoteData” will be check first.

  • Also, “Child is checked before Parent” i.e. child will decide the super() function for the parent. For example, super() function of “QuoteData” will call the __init__ function of “parent of child class (not it’s own parent)”, hence __init__ of CSVFormat will be invoked by the “super()” of QuoteData.

  • The correct order of inheritance can be checked using ‘help’ as below. If we have super() in all classes. Then super() of MixinQuoteText will call the “QuoteData”; then super() of QuoteData will call the TextFormat and so on.

>>> from contributor import *
>>> help(MixinQuoteText)
class MixinQuoteText(QuoteData, TextFormat)
 |  Text format for table
 |
 |  Method resolution order:
 |      MixinQuoteText
 |      QuoteData
 |      TextFormat
 |      TableFormat
 |      builtins.object

Warning

It is not a good idea to define a class with one method. But this feature can be quite powerful in the case of inheritance, as shown in below example.

Listing 10.9 Adding quotes around printed data using Mixin
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# contributor.py
# user-2: additional features will be added to pythonic.py by the contributor

import sys
import csv
from abc import ABC, abstractmethod

from pythonic import Ring


# Abstract class for table-format
class TableFormat(object):
    """ Abastract class """

    # print data to file or screen
    def __init__(self, out_file=None):
        if out_file == None:
            # stdout is the location where python prints the output
            out_file = sys.stdout
        self.out_file = out_file

    @abstractmethod
    def heading(self, header): # must be implemented in child class
        pass

    @abstractmethod
    def row(self, row_data): # must be implemented in child class
        pass


# text format for table
class TextFormat(TableFormat):
    """ Text format for table """

    # option for modifying width
    def __init__(self, width=7, out_file=None): # override init
        # inherit parent init as well to save data in file
        super().__init__(out_file)
        self.width = width

    def heading(self, header): # print headers
        for h in header:
            print("{0:>{1}s}".format(h, self.width),
                    end=' ', file=self.out_file)
        print(file=self.out_file)

    def row(self, row_data): # print rows
        for r in row_data:
            print("{0:>{1}s}".format(r, self.width),
                    end=' ', file=self.out_file)
        print(file=self.out_file)


# csv format for table
class CSVFormat(TableFormat):
    """ Text format for table """

    # init will be inherited from parent to save data in file

    def heading(self, header): # print headers
        print(','.join(header), file=self.out_file)

    def row(self, row_data): # print rows
        print(",".join(row_data), file=self.out_file)


# Put quotes around data : This class will be used with other class in Mixin
class QuoteData(object):
    def row(self, row_data):
        quoted_data = ['"{0}"'.format(r) for r in row_data]
        super().row(quoted_data) # nature of super() is decided by child class

# Mixin : QuoteData and CSVFormat
# this will decide the nature of super() in QuoteData
class MixinQuoteCSV(QuoteData, CSVFormat):
    pass

# Mixin : QuoteData and TextFormat
class MixinQuoteText(QuoteData, TextFormat):
    pass


# this code is copied from datamine.py file and modified slightly
# to include the type of the data
def read_file(filename, types, mode='warn'):
    ''' read csv file and save data in the list '''

    # check for correct mode
    if mode not in ['warn', 'silent', 'stop']:
        raise ValueError("possible modes are 'warn', 'silent', 'stop'")

    ring_data = [] # create empty list to save data

    with open (filename, 'r') as f:
        rows =  csv.reader(f)
        header = next(rows) # skip the header

        # change the types of the columns
        for row in rows:
            try:
                # row[2] = float(row[2]) # radius
                # row[3] = float(row[3]) # price
                # row[4] = int(row[4]) # quantity

                # generalized conversion
                row = [d_type(val) for d_type, val in zip(types, row)]
            except ValueError as err: # process value error only
                if mode == 'warn':
                    print("Invalid data, row is skipped")
                    print('Row: {}, Reason : {}'.format(row_num, err))
                elif mode == 'silent':
                    pass # do nothing
                elif mode == 'stop':
                    raise # raise the exception
                continue

            # ring_data.append(tuple(row))

            # append data in list in the form of tuple
            # row_dict = {
                    # 'date' : row[0],
                    # 'metal' : row[1],
                    # 'radius' : row[2],
                    # 'price' : row[3],
                    # 'quantity' : row[4]
                # }

            # row_dict = Ring(row[0], row[1], row[2], row[3], row[4])
            # # use below or above line
            row_dict = Ring(
                    date = row[0],
                    metal = row[1],
                    radius = row[2],
                    price = row[3],
                    quantity = row[4]
                )

            ring_data.append(row_dict)

    return ring_data


# table formatter
def print_table(list_name, col_name=['metal', 'radius'],
        out_format=TextFormat(out_file=None)): # note that class is passed here
    """ print the formatted output """

    # for c in col_name: # print header
        # print("{:>7s}".format(c), end=' ')
    # print() # print empty line
    # for l in list_name: # print values
        # for c in col_name:
            # print("{:>7s}".format(str(getattr(l, c))), end=' ')
        # print()

    # invoke class-method for printing heading
    out_format.heading(col_name) # class is passed to out_format
    for l in list_name:
        # store row in a list
        row_data = [str(getattr(l, c)) for c in col_name]
        out_format.row(row_data) # pass rows to class-method row()


def main():
    # list correct types of columns in csv file
    types =[str, str, float, float, int]

    # read file and save data in list
    list_data = read_file('price.csv', types)

    # # formatted output
    # print_table(list_data)
    # print()
    # print_table(list_data, ['metal', 'radius', 'price'],
            # out_format=TextFormat())

    # print in file
    # out_file = open("text_format.txt", "w") # open file in write mode
    # print_table(list_data, ['metal', 'radius', 'price'],
            # out_format=TextFormat(out_file=out_file))
    # out_file.close()

    # print in file with width = 15
    # out_file = open("wide_text_format.txt", "w") # open file in write mode
    # print_table(list_data, ['metal', 'radius', 'price'],
            # out_format=TextFormat(width=15, out_file=out_file))
    # out_file.close()

    # print()
    # print_table(list_data, ['metal', 'radius', 'price'],
            # out_format=CSVFormat())
    # # print in file
    # out_file = open("csv_format.csv", "w") # open file in write mode
    # print_table(list_data, ['metal', 'radius', 'price'],
            # out_format=CSVFormat(out_file=out_file))
    # out_file.close()


    print_table(list_data, ['metal', 'radius', 'price'],
            out_format=MixinQuoteText())

    print()
    print_table(list_data, ['metal', 'radius', 'price'],
            out_format=MixinQuoteCSV())

if __name__ == '__main__':
    main()

10.12. Conclusion

In this chapter, we saw some of the OOPs feature of Python especially Inheritance. More examples of Inheritance are included in Chapter 13. We discussed some of the differences between Python and other programming language. In next chapter, we will discuss ‘@property’ and ‘descriptor’ etc. Also, in the next chapter, we will discuss some more differences between Python and other programming language.