Reading Time: 7 minutesInheritance 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): |
>>> class Car(Automobile): pass |
>>> ferrari = Car( "Ferrari" ) |
>>> ferrari.ignition() |
>>> ferrari.make |
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): |
print ( "Printing make from inside the base class:" , make) |
>>> class Car(Automobile): |
def __init__( self , 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): |
print ( "Doing something with the make" ) |
>>> 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 ): |
>>> class SeaAnimal( object ): |
>>> class Croc(LandAnimal, SeaAnimal): pass |
>>> croccy = Croc() |
>>> croccy.canWalk |
>>> croccy.canSwim |
>>> croccy.walk() |
>>> croccy.swim() |
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?
>>> class A( object ): |
print ( "Doing something in A..." ) |
>>> class B(A): pass |
>>> class C( object ): |
print ( "Doing something in C..." ) |
>>> class D(B,C): pass |
>>> d.doSomething() |
[< 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
>>> class A( object ): |
print ( "Doing something in A..." ) |
>>> class B(A): pass |
print ( "Doing something in C..." ) |
>>> class D(B,C): pass |
>>> d.doSomething() |
[< class '__main__.D' >, < class '__main__.B' >, < class '__main__.C' >, < class '__main__.A' >, < class 'object' >] |
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): |
>>> class EnglishSpeaker(Speaker): |
>>> class FrenchSpeaker(Speaker): |
>>> mark = EnglishSpeaker( 'Mark' ) |
>>> pierre = FrenchSpeaker( 'Pierre' ) |
>>> mark.speak() |
>>> pierre.speak() |
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 ): |
>>> class Wheat(Crop): pass |
>>> class Corn(Crop): pass |
>>> class Tomato(Crop): pass |
>>> wheat = Wheat() |
>>> corn = Corn() |
>>> tomato = Tomato() |
>>> wheat.irrigate() |
>>> wheat.harvest() |
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.
>>> class Crop(metaclass = abc.ABCMeta): |
>>> class Wheat(Crop): pass |
>>> wheatPatch = Wheat() |
Traceback (most recent call last): |
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 ): |
raise NotImplementedError |
raise NotImplementedError |
>>> class Wheat(Crop): pass |
>>> wheatPatch = Wheat() |
>>> wheatPatch.sow() |
Traceback (most recent call last): |
raise 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:
- Inherit it as it is: use the method in the base class as-is.
- Override it: define its own functionality.
- Provide it: define the body of an abstract method.
- 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:
>>> class BaseClass(metaclass = abc.ABCMeta): |
print ( "Method: Inherit it as it as; Class: BaseClass" ) |
print ( "Method: Override it; Class: BaseClass" ) |
print ( "Method: Provide it; Class: BaseClass" ) |
print ( "Method: Extend it; Class: BaseClass" ) |
>>> class DerivedClass(BaseClass): |
print ( "Method: Override it; Class: DerivedClass" ) |
print ( "Method: Provide it; Class: DerivedClass" ) |
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
- 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.
- 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.
- 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
def processRequest(request): |
class ConcreteHandlerOne(AbstractHandler): |
def processRequest(request): |
class ConcreteHandlerTwo(AbstractHandler): |
def processRequest(request): |
class DefaultHandler(AbstractHandler): |
def processRequest(request): |
Actual code to realize the Chain of Responsibility Patttern
class AbstractHandler(metaclass = abc.ABCMeta): |
def __init__( self , successor): |
self ._successor = successor |
def handle( self , request): |
handled = self .processRequest(request) |
self ._successor.handle(request) |
def processRequest( self , request): pass |
class ConcreteHandlerOne(AbstractHandler): |
def processRequest( self , request): |
if 0 < request < = 10 : |
print ( "This is {} handling request '{}'" . format ( self .__class__.__name__, request)) |
class ConcreteHandlerTwo(AbstractHandler): |
def processRequest( self , request): |
if 10 < request < = 20 : |
print ( "This is {} handling request '{}'" . format ( self .__class__.__name__, request)) |
class DefaultHandler(AbstractHandler): |
def processRequest( self , request): |
print ( "This is {} telling you that request '{}' has no handler right now." . format ( self .__class__.__name__, request)) |
requests = [ 6 , 12 , 24 , 18 ] |
handlerSequence = ConcreteHandlerOne(ConcreteHandlerTwo(DefaultHandler( None ))) |
handlerSequence.handle(request) |
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!