Composition vs. Inheritance


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.

  • Composition (has a) - Composition is a concept that models a has a relationship between classes. It enables creating complex classes by combining objects of other classes. This means that a class may be "composed" of other classes. To put it simply, object composition occurs when a class's attributes are objects themselves.

    A good example involving object composition is an Automobile class. An Automobile class will most likely involve composition because it will have attributes that are objects themselves, like: Engine, Wheel, Window, Sound System, et cetera. The reason composition is associated with the "has a" phrase is because we can say an Automobile "has a(n)" Engine, Wheels, Windows, Sound System, et cetera. In other words, an Automobile is "composed" of those parts, hence, object composition. (See Example Below)

  • Inheritance (is a) - Inheritance is a concept that models an is a relationship between classes. Inheritance is the mechanism of basing a class upon another class, while retaining similar implementation. In this case, we are deriving a new class (called a child, derived, or sub class) from an existing class (called a parent, base, or super class) and then forming them into an hierarchy of classes. With inheritance, the child class inherits everything that the parent class has (attributes & behaviors), with the exception of constructors, while maintaining encapsulation. After a child class inherits from its parent (basically getting a copy of all of its code), you can then make changes to any existing code and/or make additional attributes & behaviors.

    A good example involving object inheritance is, again, an Automobile class. Once we are finished designing the Automobile class, we can easily define a Car class, or Truck class, or even a MonsterTruck class. This makes sense because a Car "is an" Automobile, and a Truck "is an" Automobile, and going a step further, a MonsterTruck "is a" Truck. Therefore, we can utilize inheritance to take everything (attributes & behaviors) that the parent class has, because it makes sense for the child to have those things as well. (See Example Below)

Object Composition vs Inheritance Illustration


The Object Class & Class Hierarchies


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.

In [1]:
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")
object Members:
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

Class1 Members:
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

Class2 Members:
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

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.

Class Hierarchy Chart Illustration

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.


Method Overriding & Adding Attributes/Behaviors


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.

In [2]:
# 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 Animal, Benny, is 12 years old.
The Bear, Yogi, is 90 years old.
Yogi is hybernating.
Benny is eating.
Yogi is eating.


The Super Keyword & Encapsulation


  • Super Keyword - The super keyword gives you visibility from a child class into its parent (super) class. It is used to access the parent's public attributes, public functions, and constructor. It is important to note that the super keyword is "one-way" visibility from the child to the parent, but not the other way around. Also, the super keyword is commonly used to call the parent's constructor from the child's constructor (see an example of this below within the Bear class's constructor) and to eliminate code redundancies in general.

  • Encapsulation - Although you can call parent attributes and functions using the super keyword, encapsulation cannot be broken. Therefore, any attributes and behaviors that are declared private within the parent class will not be accessible by the child. However, you can work around this by creating public getters/setters within the parent class (see an example of this below in the checkIfHybernating() function within the Bear class).

In [3]:
# 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()
The Animal, Benny, is 12 years old.
The Bear, Yogi, is 90 years old.
Yogi is hybernating.
Benny is eating.
Yogi is eating.