Virtual assistance

Python Inheritance

Inheritance allows a class to inherit attributes and methods from another class. Learn single inheritance, multiple inheritance, method overriding, and the super() function.

Python Inheritance

What is Inheritance?

Inheritance is a fundamental concept in object-oriented programming that allows a class (child class) to inherit attributes and methods from another class (parent class). This promotes code reusability and establishes a relationship between classes.

"Inheritance allows child classes to inherit and extend the functionality of parent classes."

Single Inheritance

A child class inherits from one parent class:

class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return "Some generic animal sound"
    
    def eat(self):
        return f"{self.name} is eating"

class Dog(Animal):
    def speak(self):
        return f"{self.name} says Woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow!"

# Creating instances
dog = Dog("Buddy")
cat = Cat("Whiskers")

print(dog.speak())    # Buddy says Woof!
print(cat.speak())    # Whiskers says Meow!
print(dog.eat())      # Buddy is eating
print(cat.eat())      # Whiskers is eating

The super() Function

super() allows access to parent class methods:

class Vehicle:
    def __init__(self, make, model):
        self.make = make
        self.model = model
    
    def get_info(self):
        return f"{self.make} {self.model}"

class Car(Vehicle):
    def __init__(self, make, model, year, color):
        super().__init__(make, model)  # Call parent constructor
        self.year = year
        self.color = color
    
    def get_info(self):
        parent_info = super().get_info()  # Call parent method
        return f"{parent_info} ({self.year}) - {self.color}"

car = Car("Toyota", "Camry", 2020, "Blue")
print(car.get_info())  # Toyota Camry (2020) - Blue

Method Overriding

Child classes can override parent methods:

class Shape:
    def area(self):
        return 0
    
    def perimeter(self):
        return 0

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height
    
    def perimeter(self):
        return 2 * (self.width + self.height)

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return 3.14159 * self.radius ** 2
    
    def perimeter(self):
        return 2 * 3.14159 * self.radius

rect = Rectangle(5, 3)
circle = Circle(4)

print(f"Rectangle area: {rect.area()}")        # 15
print(f"Rectangle perimeter: {rect.perimeter()}")  # 16
print(f"Circle area: {circle.area()}")          # 50.26544
print(f"Circle perimeter: {circle.perimeter()}") # 25.13272

Multiple Inheritance

A class can inherit from multiple parent classes:

class Flyable:
    def fly(self):
        return "Flying high!"

class Swimmable:
    def swim(self):
        return "Swimming gracefully!"

class Duck(Flyable, Swimmable):
    def __init__(self, name):
        self.name = name
    
    def quack(self):
        return f"{self.name} says Quack!"

duck = Duck("Donald")
print(duck.fly())     # Flying high!
print(duck.swim())    # Swimming gracefully!
print(duck.quack())   # Donald says Quack!

Method Resolution Order (MRO)

Python uses C3 linearization for MRO:

class A:
    def method(self):
        return "Method from A"

class B(A):
    def method(self):
        return "Method from B"

class C(A):
    def method(self):
        return "Method from C"

class D(B, C):
    pass

d = D()
print(d.method())     # Method from B (B comes first in inheritance)
print(D.__mro__)      # (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

Multilevel Inheritance

Inheritance through multiple levels:

class GrandParent:
    def __init__(self, name):
        self.name = name
    
    def introduce(self):
        return f"Hello, I'm {self.name}"

class Parent(GrandParent):
    def __init__(self, name, occupation):
        super().__init__(name)
        self.occupation = occupation
    
    def work(self):
        return f"I work as a {self.occupation}"

class Child(Parent):
    def __init__(self, name, occupation, hobby):
        super().__init__(name, occupation)
        self.hobby = hobby
    
    def play(self):
        return f"I love {self.hobby}"

child = Child("Alice", "Engineer", "painting")
print(child.introduce())  # Hello, I'm Alice
print(child.work())       # I work as a Engineer
print(child.play())       # I love painting

Hierarchical Inheritance

Multiple child classes inherit from one parent:

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
    
    def get_info(self):
        return f"Name: {self.name}, Salary: ${self.salary}"

class Manager(Employee):
    def __init__(self, name, salary, department):
        super().__init__(name, salary)
        self.department = department
    
    def manage_team(self):
        return f"Managing {self.department} department"

class Developer(Employee):
    def __init__(self, name, salary, language):
        super().__init__(name, salary)
        self.language = language
    
    def code(self):
        return f"Coding in {self.language}"

class Designer(Employee):
    def __init__(self, name, salary, tool):
        super().__init__(name, salary)
        self.tool = tool
    
    def design(self):
        return f"Designing with {self.tool}"

manager = Manager("John", 80000, "IT")
developer = Developer("Jane", 70000, "Python")
designer = Designer("Bob", 65000, "Figma")

print(manager.get_info())      # Name: John, Salary: $80000
print(manager.manage_team())   # Managing IT department
print(developer.code())        # Coding in Python
print(designer.design())       # Designing with Figma

Hybrid Inheritance

Combination of multiple and multilevel inheritance:

class Person:
    def __init__(self, name):
        self.name = name

class Student(Person):
    def __init__(self, name, grade):
        super().__init__(name)
        self.grade = grade

class Teacher(Person):
    def __init__(self, name, subject):
        super().__init__(name)
        self.subject = subject

class TeachingAssistant(Student, Teacher):
    def __init__(self, name, grade, subject, hours):
        Student.__init__(self, name, grade)
        Teacher.__init__(self, name, subject)
        self.hours = hours
    
    def assist(self):
        return f"Assisting {self.hours} hours per week"

ta = TeachingAssistant("Mike", "A", "Computer Science", 20)
print(f"Name: {ta.name}")           # Name: Mike
print(f"Grade: {ta.grade}")         # Grade: A
print(f"Subject: {ta.subject}")     # Subject: Computer Science
print(ta.assist())                  # Assisting 20 hours per week

Using isinstance() and issubclass()

Check inheritance relationships:

class Animal:
    pass

class Dog(Animal):
    pass

class Cat(Animal):
    pass

dog = Dog()
cat = Cat()

print(isinstance(dog, Dog))      # True
print(isinstance(dog, Animal))   # True
print(isinstance(dog, Cat))      # False

print(issubclass(Dog, Animal))   # True
print(issubclass(Cat, Animal))   # True
print(issubclass(Dog, Cat))      # False

Abstract Base Classes (ABC)

Define interfaces that must be implemented:

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass
    
    @abstractmethod
    def perimeter(self):
        pass

class Square(Shape):
    def __init__(self, side):
        self.side = side
    
    def area(self):
        return self.side ** 2
    
    def perimeter(self):
        return 4 * self.side

# This would raise an error if area() or perimeter() weren't implemented
square = Square(5)
print(f"Area: {square.area()}")          # 25
print(f"Perimeter: {square.perimeter()}") # 20

Composition vs Inheritance

When to use composition instead of inheritance:

# Inheritance (IS-A relationship)
class Car(Vehicle):
    pass  # A car IS A vehicle

# Composition (HAS-A relationship)
class Car:
    def __init__(self):
        self.engine = Engine()  # A car HAS AN engine
    
    def start(self):
        self.engine.start()

# Better approach for complex relationships
class Team:
    def __init__(self):
        self.members = []  # Team HAS members
    
    def add_member(self, member):
        self.members.append(member)

class Player:
    def __init__(self, name):
        self.name = name

team = Team()
player = Player("John")
team.add_member(player)  # Team has players, not inherits from them

Best Practices

  • Use inheritance for IS-A relationships: Dog IS AN Animal
  • Use composition for HAS-A relationships: Car HAS AN Engine
  • Keep inheritance hierarchies shallow: Avoid deep inheritance chains
  • Use super() for proper method resolution: Especially in multiple inheritance
  • Override methods appropriately: Maintain Liskov Substitution Principle
  • Use abstract base classes for interfaces: Define contracts
  • Document inheritance relationships: Use docstrings
  • Avoid diamond problem complications: Careful with multiple inheritance
  • Prefer composition over inheritance: When in doubt
  • Use isinstance() for type checking: Not type() == comparison

Common Patterns

# Template Method Pattern
class Game:
    def play(self):
        self.initialize()
        self.start_play()
        self.end_play()
    
    def initialize(self):
        pass
    
    def start_play(self):
        pass
    
    def end_play(self):
        pass

class Chess(Game):
    def initialize(self):
        print("Setting up chess board")
    
    def start_play(self):
        print("Playing chess")
    
    def end_play(self):
        print("Chess game finished")

# Factory Method Pattern
class AnimalFactory:
    @staticmethod
    def create_animal(animal_type, name):
        if animal_type == "dog":
            return Dog(name)
        elif animal_type == "cat":
            return Cat(name)
        else:
            raise ValueError("Unknown animal type")

# Strategy Pattern
class SortStrategy:
    def sort(self, data):
        pass

class BubbleSort(SortStrategy):
    def sort(self, data):
        # Bubble sort implementation
        return sorted(data)

class QuickSort(SortStrategy):
    def sort(self, data):
        # Quick sort implementation
        return sorted(data)

class Sorter:
    def __init__(self, strategy):
        self.strategy = strategy
    
    def sort_data(self, data):
        return self.strategy.sort(data)

sorter = Sorter(BubbleSort())
print(sorter.sort_data([3, 1, 4, 1, 5]))  # [1, 1, 3, 4, 5]

Inheritance is a powerful feature that enables code reuse and establishes relationships between classes. However, it should be used judiciously - composition is often a better choice for complex relationships.