Inheritance: Code Reuse and Hierarchy
Welcome to Inheritance! Think of it as family inheritance - child classes automatically get properties and methods from their parent classes, but can also add their own unique features.
What is Inheritance?
Inheritance allows you to create new classes based on existing classes. The new class (child) inherits all the attributes and methods of the existing class (parent), and can add new features or modify existing ones.
Basic Inheritance
# Parent class (base class)
class Animal:
def __init__(self, name, species):
self.name = name
self.species = species
def make_sound(self):
return "Some generic animal sound"
def eat(self, food):
return f"{self.name} eats {food}"
# Child class (derived class)
class Dog(Animal): # Inherits from Animal
def __init__(self, name, breed):
super().__init__(name, "Dog") # Call parent constructor
self.breed = breed
def make_sound(self): # Override parent method
return "Woof!"
def fetch(self): # Add new method
return f"{self.name} fetches the ball!"
# Test inheritance
generic_animal = Animal("Generic", "Unknown")
dog = Dog("Buddy", "Golden Retriever")
print(generic_animal.make_sound()) # "Some generic animal sound"
print(dog.make_sound()) # "Woof!" (overridden)
print(dog.eat("dog food")) # "Buddy eats dog food" (inherited)
print(dog.fetch()) # "Buddy fetches the ball!" (new)
The super() Function
Calling Parent Methods
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def start_engine(self):
return f"{self.make} {self.model} engine started"
def get_info(self):
return f"{self.year} {self.make} {self.model}"
class Car(Vehicle):
def __init__(self, make, model, year, num_doors):
super().__init__(make, model, year) # Call parent __init__
self.num_doors = num_doors
def start_engine(self):
# Call parent method and add extra behavior
base_message = super().start_engine()
return f"{base_message} - Car with {self.num_doors} doors ready to go!"
def get_info(self):
# Extend parent method
base_info = super().get_info()
return f"{base_info} ({self.num_doors} doors)"
# Test
car = Car("Toyota", "Camry", 2020, 4)
print(car.start_engine())
print(car.get_info())
Multiple Inheritance with super()
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
def get_power(self):
return f"{self.horsepower} HP"
class Transmission:
def __init__(self, type_):
self.type = type_
def get_transmission_info(self):
return f"{self.type} transmission"
class SportsCar(Engine, Transmission):
def __init__(self, make, model, horsepower, transmission_type):
Engine.__init__(self, horsepower) # Call Engine.__init__
Transmission.__init__(self, transmission_type) # Call Transmission.__init__
self.make = make
self.model = model
def get_specs(self):
return f"{self.make} {self.model}: {self.get_power()}, {self.get_transmission_info()}"
# Test multiple inheritance
sports_car = SportsCar("Ferrari", "488", 661, "Automatic")
print(sports_car.get_specs())
Method Overriding
Overriding Parent Methods
class Shape:
def __init__(self, name):
self.name = name
def area(self):
raise NotImplementedError("Subclasses must implement area()")
def perimeter(self):
raise NotImplementedError("Subclasses must implement perimeter()")
def describe(self):
return f"This is a {self.name}"
class Rectangle(Shape):
def __init__(self, width, height):
super().__init__("Rectangle")
self.width = width
self.height = height
def area(self): # Override abstract method
return self.width * self.height
def perimeter(self): # Override abstract method
return 2 * (self.width + self.height)
class Circle(Shape):
def __init__(self, radius):
super().__init__("Circle")
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
def perimeter(self):
return 2 * 3.14159 * self.radius
# Test
shapes = [
Rectangle(5, 3),
Circle(4)
]
for shape in shapes:
print(f"{shape.describe()}")
print(f"Area: {shape.area()}")
print(f"Perimeter: {shape.perimeter()}")
print()
Using super() in Overridden Methods
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def get_info(self):
return f"Employee: {self.name}, Salary: ${self.salary}"
def work(self):
return f"{self.name} is working"
class Manager(Employee):
def __init__(self, name, salary, department):
super().__init__(name, salary)
self.department = department
def get_info(self):
# Extend parent method
base_info = super().get_info()
return f"{base_info}, Department: {self.department}"
def work(self):
# Modify parent method
base_work = super().work()
return f"{base_work} as a manager overseeing {self.department}"
class Developer(Employee):
def __init__(self, name, salary, programming_language):
super().__init__(name, salary)
self.programming_language = programming_language
def work(self):
return f"{self.name} is coding in {self.programming_language}"
# Test
employees = [
Manager("Alice", 80000, "Engineering"),
Developer("Bob", 70000, "Python"),
Employee("Charlie", 50000)
]
for emp in employees:
print(emp.get_info())
print(emp.work())
print()
Abstract Base Classes
Using abc Module
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
def __init__(self, amount):
self.amount = amount
@abstractmethod
def process_payment(self):
"""Must be implemented by subclasses."""
pass
@abstractmethod
def validate_payment(self):
"""Must be implemented by subclasses."""
pass
def get_receipt(self):
return f"Payment of ${self.amount} processed"
class CreditCardProcessor(PaymentProcessor):
def __init__(self, amount, card_number, expiry_date):
super().__init__(amount)
self.card_number = card_number
self.expiry_date = expiry_date
def validate_payment(self):
# Validate card number format
if not self.card_number.replace(" ", "").isdigit():
raise ValueError("Invalid card number")
return True
def process_payment(self):
self.validate_payment()
# Process credit card payment
return f"Processed ${self.amount} via credit card ****{self.card_number[-4:]}"
class PayPalProcessor(PaymentProcessor):
def __init__(self, amount, email):
super().__init__(amount)
self.email = email
def validate_payment(self):
if "@" not in self.email:
raise ValueError("Invalid PayPal email")
return True
def process_payment(self):
self.validate_payment()
return f"Processed ${self.amount} via PayPal account {self.email}"
# Test
processors = [
CreditCardProcessor(100, "4111 1111 1111 1111", "12/25"),
PayPalProcessor(50, "user@example.com")
]
for processor in processors:
try:
result = processor.process_payment()
print(result)
print(processor.get_receipt())
except ValueError as e:
print(f"Payment failed: {e}")
print()
Multiple Inheritance
Diamond Problem and Method Resolution Order
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): # Multiple inheritance
pass
# Test method resolution order
d = D()
print(d.method()) # "Method from B" (B comes first in inheritance list)
print(D.__mro__) # Method Resolution Order: (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
Practical Multiple Inheritance
class LoggerMixin:
"""Mixin class for logging functionality."""
def log(self, message):
print(f"[{self.__class__.__name__}] {message}")
class SerializableMixin:
"""Mixin class for serialization."""
def to_dict(self):
return self.__dict__.copy()
def from_dict(self, data):
self.__dict__.update(data)
class User(LoggerMixin, SerializableMixin):
def __init__(self, name, email):
self.name = name
self.email = email
self.log(f"User {name} created")
def update_email(self, new_email):
old_email = self.email
self.email = new_email
self.log(f"Email changed from {old_email} to {new_email}")
# Test mixins
user = User("Alice", "alice@example.com")
user.update_email("alice@newdomain.com")
# Serialization
user_data = user.to_dict()
print("Serialized:", user_data)
# Create new user from data
new_user = User("", "")
new_user.from_dict(user_data)
print(f"Deserialized: {new_user.name}, {new_user.email}")
isinstance() and issubclass()
Type Checking with Inheritance
class Animal:
pass
class Dog(Animal):
pass
class Cat(Animal):
pass
class Labrador(Dog):
pass
# Test inheritance relationships
animals = [Animal(), Dog(), Cat(), Labrador()]
for animal in animals:
print(f"{animal.__class__.__name__}:")
print(f" isinstance(animal, Animal): {isinstance(animal, Animal)}")
print(f" isinstance(animal, Dog): {isinstance(animal, Dog)}")
print()
print("Class relationships:")
print(f"issubclass(Dog, Animal): {issubclass(Dog, Animal)}")
print(f"issubclass(Cat, Animal): {issubclass(Cat, Animal)}")
print(f"issubclass(Labrador, Dog): {issubclass(Labrador, Dog)}")
print(f"issubclass(Labrador, Animal): {issubclass(Labrador, Animal)}")
print(f"issubclass(Animal, Dog): {issubclass(Animal, Dog)}")
Practical Examples
Example 1: Employee Management System
from abc import ABC, abstractmethod
class Employee(ABC):
def __init__(self, name, employee_id, salary):
self.name = name
self.employee_id = employee_id
self.salary = salary
@abstractmethod
def calculate_payroll(self):
pass
def get_info(self):
return f"{self.name} (ID: {self.employee_id}) - ${self.salary}/year"
class SalariedEmployee(Employee):
def calculate_payroll(self):
# Monthly salary
return self.salary / 12
class HourlyEmployee(Employee):
def __init__(self, name, employee_id, hourly_rate, hours_worked):
super().__init__(name, employee_id, 0) # Salary not used
self.hourly_rate = hourly_rate
self.hours_worked = hours_worked
def calculate_payroll(self):
return self.hourly_rate * self.hours_worked
class Manager(SalariedEmployee):
def __init__(self, name, employee_id, salary, bonus_percentage):
super().__init__(name, employee_id, salary)
self.bonus_percentage = bonus_percentage
def calculate_payroll(self):
base_pay = super().calculate_payroll()
bonus = base_pay * (self.bonus_percentage / 100)
return base_pay + bonus
# Test
employees = [
SalariedEmployee("Alice", "001", 60000),
HourlyEmployee("Bob", "002", 25, 160),
Manager("Charlie", "003", 80000, 10)
]
for emp in employees:
print(f"{emp.get_info()}")
print(f"Monthly payroll: ${emp.calculate_payroll():.2f}")
print()
Example 2: Game Character System
class Character:
def __init__(self, name, health, attack_power):
self.name = name
self.health = health
self.max_health = health
self.attack_power = attack_power
self.level = 1
def attack(self, target):
damage = self.attack_power
target.take_damage(damage)
return f"{self.name} attacks {target.name} for {damage} damage!"
def take_damage(self, damage):
self.health -= damage
if self.health <= 0:
self.health = 0
return f"{self.name} has been defeated!"
return f"{self.name} takes {damage} damage, {self.health} health remaining"
def heal(self, amount):
old_health = self.health
self.health = min(self.max_health, self.health + amount)
healed = self.health - old_health
return f"{self.name} heals for {healed} health"
class Warrior(Character):
def __init__(self, name):
super().__init__(name, health=150, attack_power=20)
self.rage = 0
def attack(self, target):
# Warriors build rage
self.rage = min(100, self.rage + 20)
return super().attack(target)
def special_attack(self, target):
if self.rage >= 50:
damage = self.attack_power * 2
self.rage -= 50
target.take_damage(damage)
return f"{self.name} unleashes a powerful attack for {damage} damage!"
return f"{self.name} doesn't have enough rage for special attack"
class Mage(Character):
def __init__(self, name):
super().__init__(name, health=100, attack_power=15)
self.mana = 100
self.max_mana = 100
def attack(self, target):
if self.mana >= 10:
self.mana -= 10
return super().attack(target)
return f"{self.name} doesn't have enough mana to attack"
def cast_spell(self, target):
if self.mana >= 30:
self.mana -= 30
damage = self.attack_power * 3
target.take_damage(damage)
return f"{self.name} casts a fireball for {damage} damage!"
return f"{self.name} doesn't have enough mana to cast spell"
def meditate(self):
self.mana = min(self.max_mana, self.mana + 50)
return f"{self.name} meditates and restores mana"
# Test
warrior = Warrior("Conan")
mage = Mage("Merlin")
print("Battle begins!")
print(warrior.attack(mage))
print(mage.cast_spell(warrior))
print(warrior.special_attack(mage))
print(mage.meditate())
print(mage.attack(warrior))
Example 3: File System Hierarchy
from abc import ABC, abstractmethod
import os
from pathlib import Path
class FileSystemItem(ABC):
def __init__(self, name, parent=None):
self.name = name
self.parent = parent
self.created_time = "2024-01-01" # Simplified
@abstractmethod
def get_size(self):
pass
def get_path(self):
if self.parent:
return f"{self.parent.get_path()}/{self.name}"
return self.name
class File(FileSystemItem):
def __init__(self, name, size, parent=None):
super().__init__(name, parent)
self.size = size
def get_size(self):
return self.size
def get_extension(self):
return Path(self.name).suffix
class Directory(FileSystemItem):
def __init__(self, name, parent=None):
super().__init__(name, parent)
self.children = []
def add_item(self, item):
item.parent = self
self.children.append(item)
def get_size(self):
return sum(child.get_size() for child in self.children)
def list_contents(self):
return [child.name for child in self.children]
def find_files_by_extension(self, extension):
files = []
for child in self.children:
if isinstance(child, File) and child.get_extension() == extension:
files.append(child)
elif isinstance(child, Directory):
files.extend(child.find_files_by_extension(extension))
return files
# Test
root = Directory("root")
documents = Directory("documents")
pictures = Directory("pictures")
root.add_item(documents)
root.add_item(pictures)
# Add files
documents.add_item(File("resume.pdf", 2048))
documents.add_item(File("letter.docx", 1024))
pictures.add_item(File("vacation.jpg", 5120))
pictures.add_item(File("family.png", 3072))
print(f"Root directory size: {root.get_size()} bytes")
print(f"Documents: {documents.list_contents()}")
print(f"Picture files: {[f.name for f in pictures.find_files_by_extension('.jpg')]}")
# Find all PDF files
pdf_files = root.find_files_by_extension('.pdf')
print(f"PDF files: {[f.name for f in pdf_files]}")
Practice Exercises
Exercise 1: Vehicle Hierarchy
Create a vehicle inheritance hierarchy:
- Base
Vehicleclass Car,Truck,Motorcyclesubclasses- Each with specific attributes and methods
- Demonstrate polymorphism
Exercise 2: Shape Calculator
Build a shape calculation system:
- Abstract
Shapebase class Circle,Rectangle,Trianglesubclasses- Implement area and perimeter calculations
- Add shape-specific methods
Exercise 3: Employee Payroll System
Create an employee management system:
- Base
Employeeclass Manager,Developer,Internsubclasses- Different payroll calculation methods
- Bonus systems for managers
Exercise 4: Game Item System
Build a game item inheritance system:
- Base
Itemclass Weapon,Armor,Consumablesubclasses- Different behaviors for each type
- Inventory management
Exercise 5: Document Management
Create a document management system:
- Base
Documentclass PDF,WordDocument,Spreadsheetsubclasses- Different processing methods
- Search and filter functionality
Summary
Inheritance enables code reuse and creates class hierarchies:
Basic Inheritance:
class Child(Parent):
def __init__(self, ...):
super().__init__(...)
# Child-specific initialization
Method Overriding:
class Child(Parent):
def some_method(self):
# Override parent method
return "Child implementation"
Abstract Base Classes:
from abc import ABC, abstractmethod
class AbstractClass(ABC):
@abstractmethod
def required_method(self):
pass
Multiple Inheritance:
class Child(Parent1, Parent2):
# Inherits from multiple parents
pass
Key Concepts:
super()calls parent methods- Method Resolution Order (MRO) for multiple inheritance
- Abstract methods must be implemented by subclasses
isinstance()andissubclass()for type checking
When to Use Inheritance:
- βIs-aβ relationships (Car is a Vehicle)
- Code reuse from parent classes
- Polymorphic behavior
- Creating class hierarchies
Next: Polymorphism - same interface, different implementations! π