This lesson covers the following topics:
Inheritance and composition are two major concepts in object oriented programming that model the relationship between two classes. They drive the design of an application and determine how the application should evolve as new features are added or requirements change. In most cases, a class may utilize both composition and inheritance at the same time.
Object Class: When you create a class in Python and don't give it another class to inherit from, then it implicitly inherits from a class called object. Essentially, the object class is a template in Python that exists so all other classes can get all the essential parts that make up a class when they are created.
Below, you can see the most basic definition of two classes in Python. As you can see, even though there isn't any actual code within the classes themselves, and one class doesn't even explicitly state that it's inheritting from the object class, they both get everything that was inside of the object class regardless.
class Class1:
pass
class Class2(object):
pass
print(f"object Members:\n{dir(object)}\n\nClass1 Members:\n{dir(Class1)}\n\nClass2 Members:\n{dir(Class2)}\n")
Class Hierarchies: Now that we know that the object class is technically the parent of all classes within Python, let's show how we can choose to inherit from other classes and build an hierarchy chart to show the exact relationship between each class.
In the example above, Animal inherits from object (implicitly), Zebra inherits from Animal, Bear inherits from Animal, BlackBear inherits from Bear, and GrizzlyBear inherits from Bear.
Function (or method) overriding is the property of a class to change the implementation of a behavior (or function) provided by its parent class. Overriding is a very important part of OOP since it makes inheritance utilize its full power. By using method overriding a class may "copy" another class, avoiding duplicated code, and at the same time enhance or customize part of it.
In Python, function overriding occurs by simply defining in the child class a function with the same name of a function in the parent class. One of the most common functions that we override in a Python class is the __str__() function. This is the function that is implicitly called when you print an object. By default, this function returns a string representation of the memory location where the object is saved. Since that information isn't typically very useful, we override it to return something more useful.
Adding attributes/behaviors is also a very important part of inheritance in OOP, since it allows inheritance to add any additional attributes or behaviors that didn't already exist in the parent class, but are necessary to complete the child class. In order to add attributes or behaviors to a child class, it's as simple as writing the code in the class.
# The example below is meant to demonstrate Method-Overriding and Adding Attributes/Behaviors.
# Disclaimer: This example does not use Encapsulation properly (see Super Keyword & Encapsulation below).
class Animal:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self): # method override from object class
return f"The {type(self).__name__}, {self.name}, is {self.age} years old."
def eat(self):
print(f"{self.name} is eating.")
class Bear(Animal): # inherit everything from Animal class
def __init__(self, name, age, hybernating): # constructors are not inherited
self.name = name
self.age = age
self.hybernating = hybernating # added attribute
def checkIfHybernating(self): # added behavior
if self.hybernating:
print(f"{self.name} is hybernating.")
else:
print(f"{self.name} is not hybernating.")
benny = Animal("Benny", 12)
yogi = Bear("Yogi", 90, True)
print(benny)
print(yogi)
yogi.checkIfHybernating()
benny.eat()
yogi.eat()
# The example below is meant to demonstrate Method-Overriding and Adding Attributes/Behaviors.
# Note: This example uses Encapsulation properly (unlike the example above).
class Animal:
def __init__(self, name, age):
self.__name = name
self.__age = age
def __str__(self): # method override from object class
return f"The {type(self).__name__}, {self.__name}, is {self.__age} years old."
def eat(self):
print(f"{self.__name} is eating.")
def getName(self):
return self.__name
class Bear(Animal): # inherit everything from Animal class
def __init__(self, name, age, hybernating): # constructors are not inherited
super().__init__(name, age)
self.__hybernating = hybernating # added attribute
def checkIfHybernating(self): # added behavior
if self.__hybernating:
print(f"{super().getName()} is hybernating.")
else:
print(f"{super().getName()} is not hybernating.")
benny = Animal("Benny", 12)
yogi = Bear("Yogi", 90, True)
print(benny)
print(yogi)
yogi.checkIfHybernating()
benny.eat()
yogi.eat()