Python Inheritance Complete Guide
Learn Python inheritance - single, multiple, multilevel inheritance, method overriding, polymorphism, MRO, abstract classes, and OOP best practices with practical examples.
Inheritance
Parent-child relationships
Polymorphism
Many forms, one interface
Encapsulation
Data hiding
Abstraction
Hide complexity
What is Inheritance in Python?
Inheritance is a fundamental concept in Object-Oriented Programming (OOP) that allows a class (child/derived class) to inherit properties and methods from another class (parent/base class). It promotes code reusability and establishes relationships between classes.
Key Concept
Inheritance creates a parent-child relationship between classes. The child class inherits all attributes and methods of the parent class and can add its own specific features. Python supports multiple inheritance (a class can inherit from multiple parent classes).
# Basic Inheritance Examples
print("=== 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 info(self):
return f"{self.name} is a {self.species}"
# Child class (Derived class) - Single Inheritance
class Dog(Animal):
def __init__(self, name, breed):
# Call parent class constructor
super().__init__(name, species="Dog")
self.breed = breed
# Method overriding
def make_sound(self):
return "Woof! Woof!"
# Additional method specific to Dog
def wag_tail(self):
return f"{self.name} is wagging its tail"
# Child class (Derived class) - Another example
class Cat(Animal):
def __init__(self, name, color):
super().__init__(name, species="Cat")
self.color = color
def make_sound(self):
return "Meow! Meow!"
def climb_tree(self):
return f"{self.name} is climbing a tree"
# Using the classes
print("Creating animals:")
dog = Dog("Buddy", "Golden Retriever")
cat = Cat("Whiskers", "Orange")
print(dog.info())
print(f"Sound: {dog.make_sound()}")
print(f"Breed: {dog.breed}")
print(dog.wag_tail())
print(f"\n{cat.info()}")
print(f"Sound: {cat.make_sound()}")
print(f"Color: {cat.color}")
print(cat.climb_tree())
# Checking inheritance
print(f"\nIs Dog a subclass of Animal? {issubclass(Dog, Animal)}")
print(f"Is dog an instance of Animal? {isinstance(dog, Animal)}")
print(f"Is dog an instance of Dog? {isinstance(dog, Dog)}")
Types of Inheritance in Python
Python supports various types of inheritance. Understanding these patterns helps design better class hierarchies and relationships.
Complete Inheritance Types Reference Table
| Inheritance Type | Description | Syntax Example | When to Use | Advantages |
|---|---|---|---|---|
| Single Inheritance | One child class inherits from one parent class | class Child(Parent): | Simple parent-child relationship | Simple, clear hierarchy |
| Multiple Inheritance | One child class inherits from multiple parent classes | class Child(Parent1, Parent2): | Combining features from multiple sources | Code reuse from multiple classes |
| Multilevel Inheritance | Chain of inheritance (grandparent → parent → child) | class GrandChild(Child): | Building specialized classes gradually | Progressive specialization |
| Hierarchical Inheritance | Multiple children inherit from one parent | class Child1(Parent): class Child2(Parent): | Shared base with different specializations | Shared functionality with variations |
| Hybrid Inheritance | Combination of multiple inheritance types | Complex combinations | Complex real-world relationships | Flexibility in modeling |
Inheritance Hierarchy Visualization
Inheritance Examples with Output
Let's explore practical examples of each inheritance type with actual Python code and output.
# Single and Multilevel Inheritance Examples
print("=== Single and Multilevel Inheritance ===")
# Base class
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def display_info(self):
return f"Name: {self.name}, Age: {self.age}"
def greet(self):
return f"Hello, I'm {self.name}"
# Single Inheritance - Employee inherits from Person
class Employee(Person):
def __init__(self, name, age, employee_id, department):
super().__init__(name, age)
self.employee_id = employee_id
self.department = department
self.salary = 0
# Method overriding
def display_info(self):
base_info = super().display_info()
return f"{base_info}, ID: {self.employee_id}, Dept: {self.department}"
# Additional method
def calculate_bonus(self):
return self.salary * 0.1
# Multilevel Inheritance - Manager inherits from Employee
class Manager(Employee):
def __init__(self, name, age, employee_id, department, team_size):
super().__init__(name, age, employee_id, department)
self.team_size = team_size
self.salary = 80000 # Default salary for manager
# Further specialization
def display_info(self):
employee_info = super().display_info()
return f"{employee_info}, Team Size: {self.team_size}, Salary: ${self.salary:,.2f}"
def conduct_meeting(self):
return f"Manager {self.name} is conducting a meeting with {self.team_size} team members"
# Using the classes
print("Creating instances:")
person = Person("Alice", 30)
employee = Employee("Bob", 25, "EMP001", "Engineering")
manager = Manager("Charlie", 40, "MGR001", "Engineering", 10)
print("\nPerson:")
print(person.display_info())
print(person.greet())
print("\nEmployee:")
print(employee.display_info())
employee.salary = 60000
print(f"Bonus: ${employee.calculate_bonus():,.2f}")
print("\nManager:")
print(manager.display_info())
print(f"Bonus: ${manager.calculate_bonus():,.2f}")
print(manager.conduct_meeting())
# Check inheritance chain
print("\nInheritance checks:")
print(f"Is Manager subclass of Employee? {issubclass(Manager, Employee)}")
print(f"Is Manager subclass of Person? {issubclass(Manager, Person)}")
print(f"Is Employee subclass of Person? {issubclass(Employee, Person)}")
print(f"Is Person subclass of Employee? {issubclass(Person, Employee)}")
# Method Resolution Order (MRO)
print("\nMethod Resolution Order for Manager:")
print(Manager.__mro__)
print("\nMethod Resolution Order for Employee:")
print(Employee.__mro__)
# Multiple Inheritance and MRO Examples
print("=== Multiple Inheritance and MRO ===")
# Parent class 1
class Flyable:
def __init__(self, max_altitude):
self.max_altitude = max_altitude
def fly(self):
return f"Flying at altitude {self.max_altitude} feet"
def land(self):
return "Landing safely"
def common_method(self):
return "Method from Flyable class"
# Parent class 2
class Swimmable:
def __init__(self, max_depth):
self.max_depth = max_depth
def swim(self):
return f"Swimming at depth {self.max_depth} meters"
def dive(self):
return "Diving underwater"
def common_method(self):
return "Method from Swimmable class"
# Child class with multiple inheritance
class AmphibiousVehicle(Flyable, Swimmable):
def __init__(self, name, max_altitude, max_depth):
# Initialize both parent classes
Flyable.__init__(self, max_altitude)
Swimmable.__init__(self, max_depth)
self.name = name
def operate(self):
return f"{self.name} can both fly and swim!"
# Override common_method to resolve ambiguity
def common_method(self):
# Call specific parent's method or provide new implementation
return "AmphibiousVehicle's implementation"
# Another example with different MRO
class Vehicle:
def move(self):
return "Vehicle is moving"
def common(self):
return "From Vehicle"
class Car(Vehicle):
def move(self):
return "Car is driving on road"
def common(self):
return "From Car"
class Boat(Vehicle):
def move(self):
return "Boat is sailing on water"
def common(self):
return "From Boat"
class AmphibiousCar(Car, Boat):
def move(self):
# You can call parent methods specifically
car_move = Car.move(self)
boat_move = Boat.move(self)
return f"Amphibious Car: {car_move} and {boat_move}"
# Using the classes
print("Creating amphibious vehicle:")
amphibious = AmphibiousVehicle("Hydra", 10000, 200)
print(f"Name: {amphibious.name}")
print(amphibious.fly())
print(amphibious.swim())
print(amphibious.operate())
print(f"Common method: {amphibious.common_method()}")
print("\nCreating amphibious car:")
amphibious_car = AmphibiousCar()
print(amphibious_car.move())
print(f"Common method: {amphibious_car.common()}")
# MRO Demonstration
print("\n=== MRO Demonstration ===")
print("AmphibiousVehicle MRO:")
for i, cls in enumerate(AmphibiousVehicle.__mro__):
print(f" {i}: {cls.__name__}")
print("\nAmphibiousCar MRO:")
for i, cls in enumerate(AmphibiousCar.__mro__):
print(f" {i}: {cls.__name__}")
# Diamond Problem Example
print("\n=== Diamond Problem Example ===")
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 # No method defined, will use MRO to decide
d = D()
print(f"D's method: {d.method()}")
print("D's MRO:", [cls.__name__ for cls in D.__mro__])
# Method Overriding and super() Function Examples
print("=== Method Overriding and super() ===")
# Base class
class Shape:
def __init__(self, color):
self.color = color
self.area = 0
def calculate_area(self):
return "Area calculation not defined for generic shape"
def display_info(self):
return f"Shape color: {self.color}, Area: {self.area}"
def describe(self):
return "I am a shape"
# Child class - Circle
class Circle(Shape):
def __init__(self, color, radius):
super().__init__(color) # Call parent constructor
self.radius = radius
self.area = self.calculate_area() # Calculate area during initialization
# Method overriding
def calculate_area(self):
import math
return math.pi * self.radius ** 2
# Override display_info to add radius
def display_info(self):
base_info = super().display_info() # Call parent method
return f"{base_info}, Radius: {self.radius}"
# Additional method
def circumference(self):
import math
return 2 * math.pi * self.radius
# Child class - Rectangle
class Rectangle(Shape):
def __init__(self, color, width, height):
super().__init__(color)
self.width = width
self.height = height
self.area = self.calculate_area()
def calculate_area(self):
return self.width * self.height
def display_info(self):
return f"{super().display_info()}, Width: {self.width}, Height: {self.height}"
def perimeter(self):
return 2 * (self.width + self.height)
# Child class - Square (inherits from Rectangle)
class Square(Rectangle):
def __init__(self, color, side):
# Square is a special case of Rectangle
super().__init__(color, side, side)
# Override to provide more specific description
def describe(self):
return "I am a square (special rectangle with equal sides)"
# Don't override calculate_area - uses Rectangle's method
# Using the classes
print("Creating shapes:")
circle = Circle("Red", 5)
rectangle = Rectangle("Blue", 4, 6)
square = Square("Green", 5)
print("\nCircle:")
print(circle.display_info())
print(f"Circumference: {circle.circumference():.2f}")
print(circle.describe())
print("\nRectangle:")
print(rectangle.display_info())
print(f"Perimeter: {rectangle.perimeter()}")
print(rectangle.describe())
print("\nSquare:")
print(square.display_info())
print(f"Perimeter: {square.perimeter()}")
print(square.describe())
# Demonstrating polymorphism
print("\n=== Polymorphism Demonstration ===")
shapes = [circle, rectangle, square]
for shape in shapes:
print(f"\n{shape.__class__.__name__}:")
print(f" Area: {shape.area:.2f}")
print(f" Info: {shape.display_info()}")
print(f" Description: {shape.describe()}")
# Using super() in complex hierarchies
print("\n=== Complex super() Usage ===")
class Base:
def __init__(self):
print("Base.__init__")
self.base_value = "base"
class Middle1(Base):
def __init__(self):
print("Middle1.__init__")
super().__init__()
self.middle1_value = "middle1"
class Middle2(Base):
def __init__(self):
print("Middle2.__init__")
super().__init__()
self.middle2_value = "middle2"
class Final(Middle1, Middle2):
def __init__(self):
print("Final.__init__")
super().__init__()
self.final_value = "final"
f = Final()
print(f"\nFinal MRO: {[cls.__name__ for cls in Final.__mro__]}")
print(f"Attributes: base_value={f.base_value}, middle1_value={f.middle1_value}, "
f"middle2_value={f.middle2_value}, final_value={f.final_value}")
Polymorphism in Python
Polymorphism allows objects of different classes to be treated as objects of a common superclass. It enables methods to do different things based on the object it is acting upon.
# Polymorphism in Python
print("=== Polymorphism Examples ===")
# Base class with abstract-like method
class Animal:
def speak(self):
raise NotImplementedError("Subclass must implement abstract method")
# Different animal classes
class Dog(Animal):
def speak(self):
return "Woof!"
def fetch(self):
return "Fetching the ball"
class Cat(Animal):
def speak(self):
return "Meow!"
def climb(self):
return "Climbing the tree"
class Bird(Animal):
def speak(self):
return "Chirp!"
def fly(self):
return "Flying high"
class Cow(Animal):
def speak(self):
return "Moo!"
def give_milk(self):
return "Giving milk"
# Function demonstrating polymorphism
def animal_sounds(animals):
for animal in animals:
print(f"{animal.__class__.__name__} says: {animal.speak()}")
# Using polymorphism
print("Animal sounds:")
animals = [Dog(), Cat(), Bird(), Cow()]
animal_sounds(animals)
# Duck Typing - Python's dynamic polymorphism
print("\n=== Duck Typing ===")
class Car:
def drive(self):
return "Car is driving"
class Boat:
def drive(self):
return "Boat is sailing"
class Plane:
def drive(self):
return "Plane is flying"
# Function that works with any object having drive() method
def travel(vehicle):
print(vehicle.drive())
print("Different vehicles traveling:")
travel(Car())
travel(Boat())
travel(Plane())
# Operator Overloading
print("\n=== Operator Overloading ===")
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
# Overload + operator
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
# Overload - operator
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
# Overload * operator (scalar multiplication)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
# Overload string representation
def __str__(self):
return f"Vector({self.x}, {self.y})"
# Overload equality operator
def __eq__(self, other):
return self.x == other.x and self.y == other.y
print("Vector operations:")
v1 = Vector(2, 3)
v2 = Vector(4, 5)
print(f"v1 = {v1}")
print(f"v2 = {v2}")
print(f"v1 + v2 = {v1 + v2}")
print(f"v2 - v1 = {v2 - v1}")
print(f"v1 * 3 = {v1 * 3}")
print(f"v1 == Vector(2, 3): {v1 == Vector(2, 3)}")
print(f"v1 == v2: {v1 == v2}")
# Method Overloading (simulated)
print("\n=== Method Overloading (Simulated) ===")
class Calculator:
def add(self, *args):
if len(args) == 2:
return args[0] + args[1]
elif len(args) == 3:
return args[0] + args[1] + args[2]
else:
return sum(args)
calc = Calculator()
print(f"add(5, 10) = {calc.add(5, 10)}")
print(f"add(1, 2, 3) = {calc.add(1, 2, 3)}")
print(f"add(1, 2, 3, 4, 5) = {calc.add(1, 2, 3, 4, 5)}")
Abstract Classes and Interfaces
Abstract classes define a common interface for subclasses but cannot be instantiated themselves. They're created using the abc module.
Abstract Classes
Define common interface, cannot be instantiated:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
def describe(self):
return "I am a shape"
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
def perimeter(self):
return 2 * 3.14 * self.radius
# shape = Shape() # Error!
circle = Circle(5)
print(circle.area())
Interfaces (Python style)
Using abstract classes as interfaces:
from abc import ABC, abstractmethod
class Flyable(ABC):
@abstractmethod
def fly(self):
pass
@abstractmethod
def land(self):
pass
class Swimmable(ABC):
@abstractmethod
def swim(self):
pass
@abstractmethod
def dive(self):
pass
class Duck(Flyable, Swimmable):
def fly(self):
return "Duck flying"
def land(self):
return "Duck landing"
def swim(self):
return "Duck swimming"
def dive(self):
return "Duck diving"
duck = Duck()
print(duck.fly())
print(duck.swim())
Method Resolution Order (MRO) and super()
MRO determines the order in which base classes are searched when looking for a method. The super() function delegates method calls to the next class in the MRO.
# MRO and super() in Depth
print("=== Understanding MRO and super() ===")
class A:
def method(self):
return "A.method"
def common(self):
return "A.common"
class B(A):
def method(self):
return "B.method"
def common(self):
# Call parent's method using super()
parent_result = super().common()
return f"B.common (calling parent: {parent_result})"
class C(A):
def method(self):
return "C.method"
def common(self):
parent_result = super().common()
return f"C.common (calling parent: {parent_result})"
class D(B, C):
def method(self):
# Call B's method
b_result = B.method(self)
# Call C's method
c_result = C.method(self)
return f"D.method (B says: {b_result}, C says: {c_result})"
def common(self):
# super() will follow MRO: D -> B -> C -> A
return super().common()
# Create instance
d = D()
print("Method calls:")
print(f"d.method(): {d.method()}")
print(f"d.common(): {d.common()}")
print("\nMRO for class D:")
for i, cls in enumerate(D.__mro__):
print(f" {i}: {cls.__name__}")
print("\nUnderstanding super() calls:")
print("When d.common() is called:")
print(" 1. D.common() calls super().common()")
print(" 2. super() in D points to B (next in MRO)")
print(" 3. B.common() calls super().common()")
print(" 4. super() in B points to C (next in MRO)")
print(" 5. C.common() calls super().common()")
print(" 6. super() in C points to A (next in MRO)")
print(" 7. A.common() returns 'A.common'")
print(" 8. Results bubble back up the chain")
# Practical example with __init__
print("\n=== Practical super() in __init__ ===")
class Person:
def __init__(self, name, age):
print(f"Person.__init__ called with name={name}, age={age}")
self.name = name
self.age = age
class Employee(Person):
def __init__(self, name, age, employee_id):
print(f"Employee.__init__ called with employee_id={employee_id}")
super().__init__(name, age)
self.employee_id = employee_id
class Manager(Employee):
def __init__(self, name, age, employee_id, department):
print(f"Manager.__init__ called with department={department}")
super().__init__(name, age, employee_id)
self.department = department
print("\nCreating Manager instance:")
mgr = Manager("Alice", 35, "MGR001", "Engineering")
print(f"\nManager attributes:")
print(f" Name: {mgr.name}")
print(f" Age: {mgr.age}")
print(f" Employee ID: {mgr.employee_id}")
print(f" Department: {mgr.department}")
# C3 Linearization Algorithm
print("\n=== C3 Linearization (Python's MRO algorithm) ===")
print("Python uses C3 linearization to compute MRO.")
print("Rules:")
print(" 1. Children precede their parents")
print(" 2. Left parents precede right parents")
print(" 3. Consistency across all parent classes")
print("\nExample: class D(B, C):")
print("MRO computation:")
print(" L[D] = D + merge(L[B], L[C], B, C)")
print(" L[B] = B + merge(L[A], A) = B + merge(A, A) = B, A")
print(" L[C] = C + merge(L[A], A) = C + merge(A, A) = C, A")
print(" L[D] = D + merge(B,A, C,A, B, C)")
print(" = D + B + merge(A, C,A, C)")
print(" = D + B + C + merge(A, A)")
print(" = D + B + C + A")
print(" Final MRO: D, B, C, A")
Real-World Applications
Inheritance and polymorphism are used extensively in real-world applications. Here are practical examples:
E-commerce System
Product hierarchy with inheritance:
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def display(self):
return f"{self.name}: ${self.price}"
class Electronic(Product):
def __init__(self, name, price, warranty):
super().__init__(name, price)
self.warranty = warranty
def display(self):
return f"{super().display()}, Warranty: {self.warranty} months"
class Book(Product):
def __init__(self, name, price, author):
super().__init__(name, price)
self.author = author
def display(self):
return f"{super().display()}, Author: {self.author}"
Game Development
Character inheritance hierarchy:
class GameObject:
def __init__(self, x, y):
self.x = x
self.y = y
def draw(self):
pass
def update(self):
pass
class Character(GameObject):
def __init__(self, x, y, health):
super().__init__(x, y)
self.health = health
def take_damage(self, damage):
self.health -= damage
class Player(Character):
def draw(self):
return "Drawing player"
def update(self):
return "Updating player based on input"
class Enemy(Character):
def draw(self):
return "Drawing enemy"
def update(self):
return "Updating enemy AI"
GUI Framework
Widget inheritance structure:
class Widget:
def __init__(self, x, y, width, height):
self.x = x
self.y = y
self.width = width
self.height = height
def draw(self):
return f"Drawing widget at ({self.x}, {self.y})"
class Button(Widget):
def __init__(self, x, y, width, height, text):
super().__init__(x, y, width, height)
self.text = text
def draw(self):
return f"Drawing button '{self.text}'"
def click(self):
return f"Button '{self.text}' clicked"
class TextBox(Widget):
def __init__(self, x, y, width, height, placeholder):
super().__init__(x, y, width, height)
self.placeholder = placeholder
self.text = ""
def draw(self):
return f"Drawing text box"
Database ORM
Model inheritance patterns:
class Model:
def __init__(self):
self.created_at = datetime.now()
def save(self):
return f"Saving {self.__class__.__name__}"
def delete(self):
return f"Deleting {self.__class__.__name__}"
class User(Model):
def __init__(self, username, email):
super().__init__()
self.username = username
self.email = email
def save(self):
# Custom save logic for User
result = super().save()
return f"{result} with username {self.username}"
class Admin(User):
def __init__(self, username, email, permissions):
super().__init__(username, email)
self.permissions = permissions
Best Practices and Design Patterns
Inheritance Best Practices
- Use inheritance for "is-a" relationships
- Prefer composition over inheritance for "has-a" relationships
- Keep inheritance hierarchies shallow (2-3 levels max)
- Use abstract classes for common interfaces
- Document the purpose of each class in the hierarchy
Common Pitfalls
- Avoid deep inheritance chains (hard to maintain)
- Don't use inheritance just for code reuse
- Watch for the fragile base class problem
- Be careful with multiple inheritance complexity
- Don't override methods without calling super() when needed
- Template Method Pattern: Define algorithm skeleton in base class, let subclasses override specific steps
- Factory Method Pattern: Base class defines interface, subclasses decide which class to instantiate
- Strategy Pattern: Define family of algorithms, make them interchangeable
- Decorator Pattern: Add responsibilities to objects dynamically
- Composite Pattern: Treat individual objects and compositions uniformly
Practice Exercises
Test your inheritance knowledge with these exercises. Try to solve them before looking at solutions.
# Python Inheritance Practice Exercises
print("=== Exercise 1: Employee Hierarchy ===")
# Create a class hierarchy for different types of employees
class Employee:
def __init__(self, name, employee_id):
self.name = name
self.employee_id = employee_id
def calculate_salary(self):
raise NotImplementedError("Subclass must implement this method")
def display_info(self):
return f"ID: {self.employee_id}, Name: {self.name}"
class FullTimeEmployee(Employee):
def __init__(self, name, employee_id, monthly_salary):
super().__init__(name, employee_id)
self.monthly_salary = monthly_salary
def calculate_salary(self):
return self.monthly_salary
class PartTimeEmployee(Employee):
def __init__(self, name, employee_id, hourly_rate, hours_worked):
super().__init__(name, employee_id)
self.hourly_rate = hourly_rate
self.hours_worked = hours_worked
def calculate_salary(self):
return self.hourly_rate * self.hours_worked
class ContractEmployee(Employee):
def __init__(self, name, employee_id, contract_amount):
super().__init__(name, employee_id)
self.contract_amount = contract_amount
def calculate_salary(self):
return self.contract_amount
# Test the hierarchy
employees = [
FullTimeEmployee("Alice", "FT001", 5000),
PartTimeEmployee("Bob", "PT001", 20, 80),
ContractEmployee("Charlie", "CT001", 10000)
]
print("Employee Salaries:")
for emp in employees:
print(f"{emp.name}: ${emp.calculate_salary():,.2f}")
print("\n=== Exercise 2: Shape Hierarchy ===")
# Create shape hierarchy with area calculation
from math import pi
class Shape:
def area(self):
raise NotImplementedError
def perimeter(self):
raise NotImplementedError
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 pi * self.radius ** 2
def perimeter(self):
return 2 * pi * self.radius
class Triangle(Shape):
def __init__(self, base, height, side1, side2, side3):
self.base = base
self.height = height
self.side1 = side1
self.side2 = side2
self.side3 = side3
def area(self):
return 0.5 * self.base * self.height
def perimeter(self):
return self.side1 + self.side2 + self.side3
# Test shapes
shapes = [
Rectangle(4, 5),
Circle(3),
Triangle(6, 4, 3, 4, 5)
]
print("Shape Areas and Perimeters:")
for shape in shapes:
print(f"{shape.__class__.__name__}: Area={shape.area():.2f}, Perimeter={shape.perimeter():.2f}")
print("\n=== Exercise 3: Bank Account Hierarchy ===")
# Create bank account hierarchy
class BankAccount:
def __init__(self, account_number, balance=0):
self.account_number = account_number
self.balance = balance
def deposit(self, amount):
self.balance += amount
return f"Deposited ${amount}. New balance: ${self.balance}"
def withdraw(self, amount):
if amount > self.balance:
return "Insufficient funds"
self.balance -= amount
return f"Withdrew ${amount}. New balance: ${self.balance}"
def display_balance(self):
return f"Account {self.account_number}: Balance ${self.balance}"
class SavingsAccount(BankAccount):
def __init__(self, account_number, balance=0, interest_rate=0.01):
super().__init__(account_number, balance)
self.interest_rate = interest_rate
def add_interest(self):
interest = self.balance * self.interest_rate
self.balance += interest
return f"Added ${interest:.2f} interest. New balance: ${self.balance:.2f}"
class CheckingAccount(BankAccount):
def __init__(self, account_number, balance=0, overdraft_limit=100):
super().__init__(account_number, balance)
self.overdraft_limit = overdraft_limit
def withdraw(self, amount):
if amount > self.balance + self.overdraft_limit:
return "Overdraft limit exceeded"
self.balance -= amount
return f"Withdrew ${amount}. New balance: ${self.balance}"
# Test bank accounts
print("Bank Account Operations:")
savings = SavingsAccount("SAV001", 1000, 0.02)
checking = CheckingAccount("CHK001", 500, 200)
print(savings.display_balance())
print(savings.deposit(500))
print(savings.add_interest())
print(f"\n{checking.display_balance()}")
print(checking.withdraw(600))
print(checking.withdraw(200))
print("\n=== Exercise 4: Vehicle Rental System ===")
# Multiple inheritance example
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def display_info(self):
return f"{self.year} {self.make} {self.model}"
class Rentable:
def __init__(self, daily_rate):
self.daily_rate = daily_rate
self.is_rented = False
def rent(self, days):
if self.is_rented:
return "Already rented"
self.is_rented = True
return f"Rented for {days} days. Total: ${self.daily_rate * days}"
def return_vehicle(self):
self.is_rented = False
return "Vehicle returned"
class Car(Vehicle, Rentable):
def __init__(self, make, model, year, daily_rate, doors):
Vehicle.__init__(self, make, model, year)
Rentable.__init__(self, daily_rate)
self.doors = doors
def display_info(self):
vehicle_info = Vehicle.display_info(self)
return f"{vehicle_info} with {self.doors} doors"
# Test rental system
print("Vehicle Rental System:")
car = Car("Toyota", "Camry", 2023, 50, 4)
print(car.display_info())
print(car.rent(3))
print(car.return_vehicle())
print("\n=== Exercise 5: Abstract Animal Zoo ===")
# Abstract classes and polymorphism
from abc import ABC, abstractmethod
class Animal(ABC):
def __init__(self, name, species):
self.name = name
self.species = species
@abstractmethod
def make_sound(self):
pass
@abstractmethod
def move(self):
pass
def display_info(self):
return f"{self.name} the {self.species}"
class Mammal(Animal):
def __init__(self, name, species, fur_color):
super().__init__(name, species)
self.fur_color = fur_color
def give_birth(self):
return f"{self.name} is giving birth to live young"
class Bird(Animal):
def __init__(self, name, species, wingspan):
super().__init__(name, species)
self.wingspan = wingspan
def lay_eggs(self):
return f"{self.name} is laying eggs"
class Lion(Mammal):
def make_sound(self):
return "Roar!"
def move(self):
return "Walking on four legs"
class Eagle(Bird):
def make_sound(self):
return "Screech!"
def move(self):
return "Flying through the air"
# Test zoo
print("Zoo Animals:")
animals = [Lion("Simba", "Lion", "Golden"), Eagle("Freedom", "Eagle", 2.5)]
for animal in animals:
print(f"\n{animal.display_info()}")
print(f"Sound: {animal.make_sound()}")
print(f"Movement: {animal.move()}")
if isinstance(animal, Mammal):
print(animal.give_birth())
if isinstance(animal, Bird):
print(animal.lay_eggs())
Key Takeaways
- Inheritance allows child classes to reuse and extend parent class functionality
- Python supports multiple inheritance using comma-separated parent classes
- Use the super() function to call parent class methods
- Method Resolution Order (MRO) determines the search order for methods in inheritance hierarchy
- Method overriding allows child classes to provide specific implementations
- Polymorphism enables objects of different classes to be treated as objects of a common superclass
- Abstract classes (using
abcmodule) define interfaces that must be implemented by subclasses - Duck typing in Python focuses on object behavior rather than type
- Operator overloading allows defining behavior for operators like
+,-,* - Use inheritance for "is-a" relationships and composition for "has-a" relationships
- Keep inheritance hierarchies shallow to avoid complexity
- The C3 linearization algorithm computes MRO in Python
- Mixins are small classes that provide specific functionality to be inherited
- Always document the purpose and relationships in your class hierarchy