Python : Object Oriented Programming (Inheritance)


Let us first understand the basics of Inheritance in python. A syntax refresher. Given below is a basic inheritance syntax in Python.


class Parent(object):
    def fun(self):
        print('Parent fun!')

class A (Parent):
    def fun(self):
        print('A fun')
        super().fun()


class B(Parent):
    def fun(self):
        print ('B fun')
        super().fun()

So here we have a parent class and we are deriving class A and B from the Parent. Then we are using super() to call the base class method from sub class.

p = Parent()
a = A()
b = B()

p.fun()
a.fun()
b.fun()

output..
Parent fun!
------------
A fun
Parent fun!
------------
B fun
Parent fun!

Now let us consider the scenaio of multiple inheritance as shown below. Here we are deriving C from A and B.

class C(A, B):
    def fun(self):
        print('C fun')
        super().fun()

c = C()
c.fun()

outputs..

C fun
A fun
B fun
Parent fun!

So here a question arises why A.fun() gets called before than B.fun(). This can be answered with a concept called as MRO (Method Resoulution Order) in Python. Every class holds a magic variable called __mro__ which stores the inheritance order. So a method call get resolved based on this order. Let us print the mro for our classes.

print(C.__mro__)

outputs..

(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class '__main__.Parent'>, <class 'object'>)

So it shows that class A get resolved before B and Parent respectively. So now let us switch the inhertiance order as shown below and see the results.

class C(B, A):
    def fun(self):
        print('C fun')
        super().fun()
        
print(C.__mro__)

outputs..

(<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class '__main__.Parent'>, <class 'object'>)

Now as expected the resoultion order is reversed with class B before A and parent.

Example : TableFormatter

Now let us write a concrete example to solidify our inheritance understanding. Here we want to create a method print_table which prints tabular data of collection passed to it. In this example we will be making the collection reading a csv data file as shown below.


# stocks.csv
**Name,Date,Shares,Price**
HPQ,7/11/2007,100,32.2
IBM,7/12/2007,50,91.9
GE,7/13/2007,150,83.44
CAT,7/14/2007,200,51.23
MSFT,7/15/2007,95,40.37
HPE,7/16/2007,50,65.1
AFL,7/17/2007,100,70.44

So as a first step let us create a collection from this data. Let Holding be the class corresponding to one row in the data.

class Holding(object):
     def __init__(self,name, date, shares, price ):
         self.name = name
         self.date = date
         self.shares = shares
         self.price = price

Now let us write a function that can display any objects in a text tabular format.The inputs to this function are list of objects and column names/attribute names of the objects. We are using getattr to get the attribute value to be displayed. (Its important that column name should match attribute.You can improve this by passing a tuple which hold display value and attribute value if we need to display diffrent column name than attribute name)

def print_table(objects, columns):
    '''
    This method prints a table from objects using getattr
    :param objects: object list to iterate
    :param columns: column names matching attributes of object
    '''
    # print header
    for col in columns:
        print('{:>10s}'.format(col), end=' ')
    print()
    for obj in objects:
        for col in columns:
            print('{:>10s}'.
                  format(str(getattr(obj, col))), end=' ')
        print()

Now let us use the above method to generate table of ‘stocks.csv’ First let us load that data into a list and then generate the table as shown below.


# program.py
import csv
def load_portfolio():
    holdings = []
    with open('stocks.csv', 'r') as f:
        rows = csv.reader(f)
        head = next(rows)
        for row_no, row in enumerate(rows):

            try:
                holdings.append(Holding(name=row[0], date=row[1],
                                    shares=int(row[2]), price=float(row[3])))

            except ValueError as ve:
                print('Ignoring row {}-{}'.format(row_no,row))

    return holdings
# print the table
print_table(load_portfolio(), ['name','date', 'shares', 'price'] ):

Now comes interesting part, what if we want to print as CSV, html tables etc.. So its good to define a contract for the formatter and derive sub classess from it, the logic of formatting can be eliminated from the print_table().

So we can rewrite print_table() as follows. As first step let us create a base class ‘Formatter’, which is like an interface that specifies the contract.

class Formatter(object):
    def print_header(self, cols):
        pass

    def print_records(self, objects):
        pass

Now let us modify our print_table() as follows.

def print_table(objects,columns, formatter):
    # print header
    formatter.print_header(columns)

    # print rows
    for obj in objects:
        formatter.print_records([str(getattr(obj, col)) 
                                 for col in columns])

Now let us implement our custom formatters.

TextFormatter..

class TextFormatter(Formatter):

    def print_header(self, headers):
        for head in headers:
            print('{:>10s}'.format(head), end=" ")
        print()

    def print_records(self, rowdata):
        for item in rowdata:
            print('{:>10s}'.format(item), end=" ")
        print()

CSVFormatter..

class CSVFormatter(Formatter):
    def print_header(self, headers):
        print(','.join(headers))
        print()

    def print_records(self, rowdata):
        print(','.join(rowdata))
        print()

HTMLFormatter..

class HTMLFormatter(Formatter):

    def print_header(self, headers):

        print('<tr>')
        for head in headers:
            print('<th>{}</th>'.format(head))
        print('</tr>')

    def print_records(self,rowdata):
        print('<tr>')
        for item in rowdata:
            print('<td>{}</td>'.format(item))
        print('</tr>')

Now we can use these formatters as shown below..

print_table(load_portfolio(), 
            ['name','date', 'shares', 'price'], 
            HTMLFormatter())

print_table(load_portfolio(), 
            ['name','date', 'shares', 'price'], 
            CSVFormatter())

print_table(load_portfolio(), 
            ['name','date', 'shares', 'price'], 
            TextFormatter())

output :

#html
<tr>
    <th>name</th>
    <th>date</th>
    <th>shares</th>
    <th>price</th>
</tr>
<tr>
    <td>HPQ</td>
    <td>7/11/2007</td>
    <td>100</td>
    <td>32.2</td>
</tr>
<tr>
    <td>IBM</td>
    <td>7/12/2007</td>
    <td>50</td>
    <td>91.9</td>
</tr>
<tr>
....
....

Multiple Inheritance

Python supports multiple inheritance. So let us consider that in our scenario above. Let us say that I have to print content in quotes. So let us implement a class that does that.

class QuoteFormatter(object):
    def print_records(self, rowdata):
        quoted = ['"{}"'.format(item) for item in rowdata]
        super().print_records(quoted)

Now let us derive a new class from this and our existing class.

class QuoteTextFormatter(QuoteFormatter, TextFormatter):
    pass

Now let us call this ..

print_table(load_portfolio(),
            ['name','date', 'shares', 'price'],
            QuoteTextFormatter())

output:

      name       date     shares      price 
     "HPQ" "7/11/2007"      "100"     "32.2" 
     "IBM" "7/12/2007"       "50"     "91.9" 
      "GE" "7/13/2007"      "150"    "83.44" 
     "CAT" "7/14/2007"      "200"    "51.23" 
    "MSFT" "7/15/2007"       "95"    "40.37" 
     "HPE" "7/16/2007"       "50"     "65.1" 
     "AFL" "7/17/2007"      "100"    "70.44" 

In multiple inheritance we know that both parent classes have an implementation of print_records, which version of that will inject into derived class. The answer for that depends on the order of base class. In our case its QuoteFormatter. If you put the other way, then you won’t see any quotes in output.

Abstract Class

In our implementation above there is a big flaw, our base class is not implementing the two main methods as shown below.

class Formatter(object):
    def __init__(self, outfile=sys.stdout):
        self.outfile = outfile

    
    def print_header(self, cols):
        pass
    
    def print_records(self, objects):
        pass

So if a developer tries to instantiate this class and pass as parameter to our print_table it will break. So in this case we have to make this class an abstract class. This will ensure that all derived class have implementation of its methods as well as avoid instantiation of this class object. We can make abstract as follows.

from abc import ABC, abstractmethod

class Formatter(ABC):
    def __init__(self, outfile=sys.stdout):
        self.outfile = outfile

    @abstractmethod
    def print_header(self, cols):
        pass

    @abstractmethod
    def print_records(self, objects):
        pass

Now if we try following code, it won’t compile.. formatter = Formatter(); TypeError: Can’t instantiate abstract class Formatter with abstract methods print_header, print_records

Another way of writing defensive coding is to ensure we are getting expected type instance using isinstance()

def print_table(objects,columns, formatter):

    if not isinstance(formatter, Formatter) :
        print('Formatter instance is expected !')
        return
    # print header
    formatter.print_header(columns)

    # print rows
    for obj in objects:
        formatter.print_records([str(getattr(obj, col))
                                 for col in columns])

Coding is fun enjoy…