Inheritance and Polymorphism

Buffer this pageShare on FacebookPrint this pageTweet about this on TwitterShare on Google+Share on LinkedInShare on StumbleUpon
Reading Time: 7 minutes

Inheritance and Polymorphism in Python

Inheritance and Polymorphism in Python

Hello. Welcome back to Object Oriented Python. In this chapter, we'll look at how Inheritance and Polymorphism work in Python.

Table of Contents


Inheritance

  • The second of 3 foundation principles of Object Oriented Programming, Inheritance refers to inheriting the attributes & methods of a class by another class.
  • The class which is being inherited may be called either Base class, Parent class or Superclass. Correspondingly, the inheriting class may be called Derived class, Child class or Subclass.
  • The superclass is passed to the subclass while defining the subclass.
>>> class Automobile:
	def __init__(self, make):
		self.make = make
	def ignition(self):
		print("Igniting...")

		
>>> class Car(Automobile): pass

>>> ferrari = Car("Ferrari")
>>> ferrari.ignition()
Igniting...
>>> ferrari.make
'Ferrari'

In the above example, the class Car inherits the attribute make, methods __init__() and ignition() from the class Automobile. The inheriting class can and usually does, define its own set of methods and attributes. Inheritance makes the code simple, readable and adds a logical hierarchy to the code.


Anomaly in defining the base class in Python 2 & 3: New Style Classes and Old Style Classes

Before we discuss this, you should know that the parent class of each and every class in Python is the class called object. Everything in Python inherits from this class. That is why it is said that everything in Python is an object. You can do a quick >>> object or >>> help(object) or >>> dir(object) to explore this further.

In Python, there are two alternative ways of defining a base class: class BaseClass(object): and class BaseClass:. In Python 3, it does not make any difference which syntax you use. In Python 2, however, it makes a big difference. Python 3 allows you to drop the word object while declaring a base class, and it will treat the base class the same way. But Python 2 does not, which may lead to a problem. This has to do with Old-style classes and New-style classes.

Python 2 has two types of classes: Old-style classes and New-style classes. The latter were introduced in Python 2.2. The difference between Old-style classes and New-style classes is that

  • In Old-style classes, obj.__class__ and type(obj) evaluate to different things. obj.__class__ gives the class name, where as type(obj) gives . This is due to the fact that all old-style objects/instances, irrespective of their classes, are implemented with a single built-in type called instance.
  • In New-style classes, obj.__class__ and type(obj) both evaluate to the same thing i.e. the class name.

Apart from this significant difference, there are two additional methods that New-style classes support which Old-style classes don't: mro() and super(). We will get to these later in the chapter.

Python 3 does not support Old-style classes. In fact, there is little chance that you will find a topic in the discussion forums relating to these in Python 3 docs. In Python 2 docs, it lies here.

In Python 2, class BaseClass(object): refer to new-style classes and class BaseClass: refer to old-style classes.

In Python 3, both class BaseClass(object): and class BaseClass: refer to the new-style classes, the latter syntax inherits the object class implicitly.

As you can see, the alternative syntax in Python 3 is the same as the syntax to declare old-style classes in Python 2. Python 2 is still very widely used, such as in Pygame, Web2Py etc. Any attempt to apply the Python 3 class definitions in Python 2 will lead to outdated base objects. And since, this is not a prominently known issue, debugging will be even more difficult.


Impact of Inheritance on attribute lookup mechanism

Inheritance adds another level of hierarchy when an attempt is made to access attribute (or a method) using an instance. The attribute is first looked for in the instance attributes. If not found, Python moves on to the class to which the instance belongs to. If the attribute is not found there, Python looks for it in the class(es) which the class of the instance inherits.


Adding arguments to the __init__() of superclass: super()

Like all other attributes and methods, the __init__() method of the superclass is inherited by the subclasses. If there is no __init__() method in the subclass, then the __init__() method of the superclass is called when the subclass is instantiated. If the subclass also defines an __init__() method, then it is called instead of the __init__() method of the superclass.

>>> class Automobile(object):
	def __init__(self, make):
		self.make = make
		print("Printing make from inside the base class:", make)
	def ignition(self):
		print("Igniting...")

		
>>> class Car(Automobile):
	def __init__(self, make):
		self.make = make
		print("Printing make from inside the derived class:", make)

		
>>> ferrari = Car('Ferrari')
Printing make from inside the derived class: Ferrari

Now, consider a situation where you want the __init__() method of your subclasses to have additional functionality along with the functionality already defined in the __init__() method of the superclass. To achieve this, you can use the builtin super() function.

>>> class Automobile(object):
	def __init__(self, make):
		self.make = make
		print("Doing something with the make")
	def ignition(self):
		print("Igniting...")

		
>>> class Car(Automobile):
	def __init__(self, make, horsepower):
		super(Car, self).__init__(make)
		self.horsepower = horsepower
		print("Doing something with the horsepower")

		
>>> ferrari = Car('Ferrari', 3200)
Doing something with the make
Doing something with the horsepower

super(Car, self).__init__(make) calls the __init__() method of the super class of Car passing the instance as an argument to it, in addition to the positional argument make. We could have easily replaced this statement with Automobile.__init__(self, make), but the super() function helps in decoupling the classes. If tomorrow, the superclass of Car changes from Automobile to, say, Cars, then we will have to change this particular statement inside the Car class as well.


Multiple Inheritance

There may be occasions when a class may inherit from more than 1 classes. In such a case, specify the superclasses while defining the subclass.

>>> class LandAnimal(object):
	canWalk = True
	def walk(self):
		print("Walking...")

		
>>> class SeaAnimal(object):
	canSwim = True
	def swim(self):
		print("Swimming...")

		
>>> class Croc(LandAnimal, SeaAnimal): pass

>>> croccy = Croc()
>>> croccy.canWalk
True
>>> croccy.canSwim
True
>>> croccy.walk()
Walking...
>>> croccy.swim()
Swimming...

Attribute Lookup in Multiple Inheritance situations: When an attribute is sought by an instance, Python looks for it in the class to which the instance belongs, then moves to its parents, then on to it so called grandparents and so on. Since Multiple Inheritance can take many forms, in certain situations, it may become difficult for the developer to comprehend Python's lookup mechanism. We can use the mro() method to determine the order in which Python looks for attributes and methods in superclasses.

Let's take a look at a couple of such situations. To avoid confusion, I'll name the classes A, B, C and D.

#1 Depth-first or Breadth-first?

# B inherits from A, D inherits from B
# D inherits from C
# The required method lies in A, as well as in C, and is called via D.
# Will Python go depth-first and call the method in A? Or will it go from B to C first and go breadth-first? Let's find out.

  __A__
	|
	|
	v
  __B__		  __C__
	|			|
	|			|
	+---> D <---+
	
>>> class A(object):
	def doSomething(self):
		print("Doing something in A...")

		
>>> class B(A): pass

>>> class C(object):
	def doSomething(self):
		print("Doing something in C...")

		
>>> class D(B,C): pass

>>> d = D()
>>> d.doSomething()
Doing something in A...

# So, Python goes for depth-first by default. We can verify this using the mro() method.

>>> D.mro()
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.A'>, <class '__main__.C'>, <class 'object'>]

MRO stands for method resolution order. Despite its name, it can be used for attributes as well. This method provides us a list of classes, the order in which Python looks for attributes. The method itself is called on the className and not on one of its instances.

#2 Diamond Problem

# B & C inherit A
# D inherits B & C
# The required method lies in A, as well as in C, and is called via D.
# From what we saw in the earlier example, the MRO should be D-B-A-C-A. Let's see if this is the case.

				A
			/		\
		/				\
	B						C
		\				/
			\		/
				D
				
>>> class A(object):
	def doSomething(self):
		print("Doing something in A...")

		
>>> class B(A): pass

>>> class C(A):
	def doSomething(self):
		print("Doing something in C...")

		
>>> class D(B,C): pass

>>> d = D()
>>> d.doSomething()
Doing something in C...

>>> D.mro()
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

# In a diamond shape pattern, the most recent reference to the grandparent class (A in this case) is considered and the previous ones are discarded. So, the calculated MRO D-B-A-C-A gets rectified to D-B-C-A.

Polymorphism

The third foundation principle of Object Oriented Programming is Polymorphism. Polymorphism, in general sense, refers to the property of an object being capable of existing in several different shapes or forms. In Python, Polymorphism refers to different classes having methods by the same name. These methods have the same functionality, achieved differently. A particular behavior maybe realized differently by objects of different classes. Theses classes are said to have the same interface, owing to the same name of the methods. The client/user can call the methods intuitively without having to worry about which instance/object is being used.

Let's look at an example.

>>> class Speaker(object):
	def __init__(self, name):
		self.name = name

		
>>> class EnglishSpeaker(Speaker):
	def speak(self):
		print("Hi")

		
>>> class FrenchSpeaker(Speaker):
	def speak(self):
		print("Salut")

		
>>> mark = EnglishSpeaker('Mark')
>>> pierre = FrenchSpeaker('Pierre')
>>> mark.speak()
Hi
>>> pierre.speak()
Salut

Abstract Classes

An Abstract Class provides a model to define several other classes. It is not supposed to be instantiated. It can be used to force methods upon its subclasses which they must implement. Let's look at a basic example first.

>>> class Crop(object):
	def sow(self):
		print("Sowing...")
	def irrigate(self):
		print("Irrigating...")
	def harvest(self):
		print("Harvesting...")

		
>>> class Wheat(Crop): pass

>>> class Corn(Crop): pass

>>> class Tomato(Crop): pass

>>> wheat = Wheat()
>>> corn = Corn()
>>> tomato = Tomato()
>>> 
>>> wheat.sow()
Sowing...
>>> wheat.irrigate()
Irrigating...
>>> wheat.harvest()
Harvesting...

The class Crop acts as an abstract class, providing a model for the Wheat, Corn and Tomato classes.

Forcing the subclasses to implement the inherited methods

There are two ways to achieve this:

  • using the builtin abc module
  • raising NotImplementedError

Using the builtin abc module

The builtin abc module helps materialize Abstract Base Classes. It has the abstractmethod() method which is a decorator indicating abstract methods. For the abstractmethod() to work, it is required that the metaclass is ABCMeta (defined in the abc module itself) or derived from it. Subclasses of any class with this setup cannot be instantiated unless all of its abstract methods are implemented.

>>> import abc
>>> class Crop(metaclass=abc.ABCMeta):
	'''Abstract class declaring that its subclasses must implement that the sow() & harvest() methods.'''
	@abc.abstractmethod
	def sow(self): pass
	
	def irrigate(self): pass
	
	@abc.abstractmethod
	def harvest(self): pass

	
>>> class Wheat(Crop): pass

>>> wheatPatch = Wheat()
Traceback (most recent call last):
    wheatPatch = Wheat()
TypeError: Can't instantiate abstract class Wheat with abstract methods harvest, sow

Raising NotImplementedError

This is a relatively straightforward way to enforce the subclasses to implement methods of the superclass. Any method which you wish the subclasses to implement, raise the NotImplementedError, ensuring that they are overridden by the inheriting classe(s).

>>> class Crop(object):
	'''Abstract class declaring that its subclasses must implement that the sow() & harvest() methods.'''
	def sow(self):
		raise NotImplementedError
	
	def irrigate(self): pass
	
	def harvest(self):
		raise NotImplementedError

	
>>> class Wheat(Crop): pass
>>> wheatPatch = Wheat()
>>> wheatPatch.sow()
Traceback (most recent call last):
    wheatPatch.sow()
    raise NotImplementedError
NotImplementedError

4 Things That Could be Done With Methods in Abstract Classes: Method Overloading

We have seen different things that can be done while inheriting methods of base class. To summarise it, there are 4 things you can do while inheriting methods of base class:

  1. Inherit it as it is: use the method in the base class as-is.
  2. Override it: define its own functionality.
  3. Provide it: define the body of an abstract method.
  4. Extend it: In addition to the functionality defined in base class, add more functionality using the super() function.

Here's a brief code example illustrating the above points:

>>> import abc
>>> class BaseClass(metaclass = abc.ABCMeta):
	def inherit(self):
		print("Method: Inherit it as it as; Class: BaseClass")
	def override(self):
		print("Method: Override it; Class: BaseClass")
	@abc.abstractmethod
	def provide(self):
		print("Method: Provide it; Class: BaseClass")
	def extend(self):
		print("Method: Extend it; Class: BaseClass")

		
>>> class DerivedClass(BaseClass):
	def override(self):
		print("Method: Override it; Class: DerivedClass")
	def provide(self):
		print("Method: Provide it; Class: DerivedClass")
	def extend(self):
		super(DerivedClass, self).extend()
		print("Method: Extend it; Class: DerivedClass")

		
>>> derivedOne = DerivedClass()
>>> derivedOne.inherit()
Method: Inherit it as it as; Class: BaseClass
>>> derivedOne.override()
Method: Override it; Class: DerivedClass
>>> derivedOne.provide()
Method: Provide it; Class: DerivedClass
>>> derivedOne.extend()
Method: Extend it; Class: BaseClass
Method: Extend it; Class: DerivedClass

Exercise: Chain of Responsibility Design Pattern

In this chapter, we studied the significant topics of Inheritance, Polymorphism and Abstract Classes. Let's put our knowledge to good use by undertaking an exercise. Our exercise is to implement a primitive version of the Chain of Responsibility Design Pattern.

The Chain of Responsibility Design Pattern caters to the requirement that different requests need different processing. Based on the request, different types of processing are needed. For example, you have a list of numbers, say 6, 12, 24, 18 and you need to process these numbers according to the range in which they lie. That is, you wish to process numbers 1-10 in one way, 11-20 in another way, and numbers not in the range 1-20 in another way. The Chain of Responsibility pattern provides a sophisticated method to handle this design.

Terminology used in the Chain of Responsibility Pattern

  1. Abstract Handler: The Abstract handler is inherited by all Concrete Handlers. It provides the following 3 methods:
    • init(): stores a successor handler who will handle the request if it is not handled by the current handler.
    • handle(): the handle() method is inherited by the concrete handlers as-is. Further, it invokes the processRequest() method of the current handler.
    • processRequest(): the processRequest() method is an abstract method. Each Concrete Handler provides its own logic to handle the requests.
  2. Concrete Handlers: Each Concrete Handler inherits from the Abstract Handler, has the handle() method by default, due to inheritance of the Abstract Handler and overrides the processRequest() method of the Abstract Handler in which it provides its own logic to handle requests.
    • init(): inherited as-is from the Abstract Handler. This method is not present in the class definition of the Concrete Handler. It stores a successor handler who will handle the request if it is not handled by the current Concrete Handler.
    • handle(): inherited as-is from the Abstract Handler. This method is also not present in the class definition of the Concrete Handler. It invokes the processRequest() of this Concrete Handler.
    • processRequest(): overridden method of the Abstract Handler. Contains the logic of each Concrete Handler to handle requests. Each Concrete Handler will have their own logic. The method returns True if request handled successfully, if not, control goes back to the handle() method which passes the request to the successor handler.
  3. Default Handler: The Default Handler receives the request after each of the Concrete Handlers have tried to process it and failed. The Default Handler also inherits from Abstract Handler, has the handle() method by default, due to inheritance of the Abstract Handler and overrides the processRequest() method of the Abstract Handler to inform the user that there is no concrete handler in place to handle this request.

Psedo Code to realize the Chain of Reaponsibility Pattern

class AbstractHandler:
        '''Abstract Handler: inherited by all concrete handlers;'''
        def __init__(successor): ''''sets the next handler to local variable "_successor"'''
        def handle(request): '''invokes the processRequest() of the current handler; if request is handled, then processing of next request begins;
								if request cannot be handled by the current handler, it is passed on to the handle() method of the successor handler.'''
        def processRequest(request): '''abstract method in which each Concrete Handler defines its own logic to handle the requests.'''

class ConcreteHandlerOne(AbstractHandler):
        '''Concrete Handler # 1: Inherits from the abstract handler; overrides the processRequest() method of the AbstractHandler; has the handle() method by default, due to inheritance of the AbstractHandler'''
        def processRequest(request): '''Attempt to handle the request; return True if handled'''

class ConcreteHandlerTwo(AbstractHandler):
        '''Concrete Handler # 2: Inherits from the abstract handler; overrides the processRequest() method of the AbstractHandler; has the handle() method by default, due to inheritance of the AbstractHandler'''
        def processRequest(request): '''Attempt to handle the request; return True if handled'''

class DefaultHandler(AbstractHandler): 
        '''Default Handler: inherits from the abstract handler; overrides the processRequest() method of the AbstractHandler; has the handle() method by default, due to inheritance of the AbstractHandler'''
        def processRequest(request): '''Provide an elegant message saying that this request has no handler. returns True to imply that even this request has been handled.'''

## USING THE ABOVE SETUP ##
# Create a list of requests to be processed.
# Define a sequence of handlers to be followed by each request until it is handled.
# Initiate the chain of responsibility: send the requests one by one, to handlers as per sequence of handlers defined above.

Actual code to realize the Chain of Responsibility Patttern

import abc

class AbstractHandler(metaclass = abc.ABCMeta):
    '''Abstract Handler: inherited by all concrete handlers.'''
    def __init__(self, successor):
        ''''sets the next handler to local variable "_successor"'''
        self._successor = successor
    def handle(self, request):
        '''invokes the processRequest() of the current handler; if request is handled, then processing of next request begins;
          if request cannot be handled by the current handler, it is passed on to the handle() method of the successor handler.'''
        handled = self.processRequest(request)
        if not handled:
            self._successor.handle(request)

    @abc.abstractmethod
    def processRequest(self, request): pass

class ConcreteHandlerOne(AbstractHandler):
    '''Concrete Handler # 1: Inherits from the abstract handler; overrides the processRequest() method of the AbstractHandler; has the handle() method by default, due to inheritance of the AbstractHandler'''
    def processRequest(self, request):
        '''Attempt to handle the request; return True if handled'''
        if 0 < request <= 10:
            print("This is {} handling request '{}'".format(self.__class__.__name__, request))
            return True

class ConcreteHandlerTwo(AbstractHandler):
    '''Concrete Handler # 2: Inherits from the abstract handler; overrides the processRequest() method of the AbstractHandler; has the handle() method by default, due to inheritance of the AbstractHandler'''
    def processRequest(self, request):
        '''Attempt to handle the request; return True if handled'''
        if 10 < request <= 20:
            print("This is {} handling request '{}'".format(self.__class__.__name__, request))
            return True

class DefaultHandler(AbstractHandler):
    '''Default Handler: inherits from the abstract handler; overrides the processRequest() method of the AbstractHandler; has the handle() method by default, due to inheritance of the AbstractHandler'''
    def processRequest(self, request):
        '''Provide an elegant message saying that this request has no handler. returns True to imply that even this request has been handled.'''
        print("This is {} telling you that request '{}' has no handler right now.".format(self.__class__.__name__, request))
        return True
				
				
# Create a list of requests to be processed.
requests = [6, 12, 24, 18]

# Define a sequence of handlers to be followed by each request until it is handled.
handlerSequence = ConcreteHandlerOne(ConcreteHandlerTwo(DefaultHandler(None)))

# Initiate the chain of responsibility: send the requests one by one, to handlers as per sequence of handlers defined above.
for request in requests:
    handlerSequence.handle(request)


### OUTPUT ###
This is ConcreteHandlerOne handling request '6'
This is ConcreteHandlerTwo handling request '12'
This is DefaultHandler telling you that request '24' has no handler right now.
This is ConcreteHandlerTwo handling request '18'

Always remember to focus on clarity and maintenance while drafting a solution for a problem. See you in the next and last chapter in the course. Cheerio!

 

Buffer this pageShare on FacebookPrint this pageTweet about this on TwitterShare on Google+Share on LinkedInShare on StumbleUpon

Leave a Reply