Object-Oriented Programming (OOP) - Part 2
Inheritance is like family traits. A child (subclass) inherits characteristics from parents (superclass) but can also have unique traits. Polymorphism is like different animals making sounds - a dog says 'woof', a cat says 'meow', but both 'make_sound()'.
Object‑Oriented Programming (OOP) Part 2 builds on the basics (classes, objects, encapsulation) and introduces **inheritance**, **polymorphism**, **abstraction**, and **composition**. These concepts allow you to create flexible, reusable, and maintainable code.
## 1. Inheritance (Is‑a Relationship)
Inheritance allows a class (child/subclass) to inherit attributes and methods from another class (parent/superclass). The child can then add new methods or override existing ones.
```python
class Animal:
def eat(self):
return "Eating..."
class Dog(Animal): # Dog inherits from Animal
def bark(self):
return "Woof!"
```
### Method Overriding
A child class can provide its own implementation of a method that already exists in the parent. Use the same method name and signature.
### `super()`
The `super()` function allows you to call a method from the parent class. It is especially useful in `__init__` to avoid repeating initialization code.
## 2. Types of Inheritance
- **Single inheritance** – one parent, one child.
- **Multi‑level inheritance** – a chain: `GrandParent -> Parent -> Child`.
- **Multiple inheritance** – a child inherits from two or more parents (Python supports this).
- **Hierarchical inheritance** – multiple children inherit from one parent.
- **Hybrid inheritance** – a combination of multiple and multi‑level (needs careful MRO handling).
## 3. Method Resolution Order (MRO)
In multiple inheritance, Python uses the C3 linearization algorithm to decide which method to call. You can view the MRO using `ClassName.__mro__`.
## 4. Abstract Base Classes (ABC)
An **abstract class** cannot be instantiated. It defines abstract methods that must be implemented by any concrete (non‑abstract) subclass. Use the `abc` module and `@abstractmethod` decorator.
```python
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Circle(Shape):
def area(self):
return 3.14 * self.radius ** 2
```
## 5. Polymorphism (Many Forms)
Polymorphism allows objects of different classes to respond to the same method call in their own way. This is achieved through:
- **Method overriding** (subclasses provide specific implementations).
- **Duck typing** – “If it walks like a duck and quacks like a duck, it's a duck.” Python doesn't require strict inheritance; any object with the required method will work.
## 6. Composition (Has‑a Relationship)
Composition means a class contains instances of other classes as attributes. It is often preferred over inheritance when there is no clear “is‑a” relationship. Composition promotes flexibility and loose coupling.
```python
class Engine:
def start(self):
return "Vroom"
class Car:
def __init__(self):
self.engine = Engine() # Car has an Engine
```
## 7. Inheritance vs. Composition
| Inheritance | Composition |
|-------------|-------------|
| “is‑a” relationship (Dog is an Animal) | “has‑a” relationship (Car has an Engine) |
| Tighter coupling | Looser coupling |
| Code reuse via extension | Code reuse via delegation |
| Overriding is natural | Requires wrapper methods |
| Can break encapsulation (fragile base class problem) | More robust to changes |
**Rule of thumb:** Prefer composition over inheritance when the relationship is not a clear hierarchy.
## 8. Real‑world Examples
- **GUI frameworks** – `Button` inherits from `Widget`, `Clickable`.
- **Game development** – `Enemy` inherits from `Character`, `FlyingEnemy` inherits from `Enemy`.
- **Plug‑in architectures** – Abstract base classes define interfaces.
- **Django models** – `class MyModel(models.Model)`.
## 9. Common Mistakes
- Forgetting to call `super().__init__()` in child classes – parent attributes are not initialised.
- Overriding a method but changing its signature – breaks polymorphic calls.
- Using multiple inheritance carelessly – leads to confusing MRO.
- Instantiating an abstract class – raises `TypeError`.
- Misusing composition when inheritance is more natural (and vice versa).
## 10. Practice Exercises
1. Create a base class `Vehicle` with `start()` and `stop()`. Derive `Car` and `Bike`, override `start()` differently.
2. Use `super()` to initialise parent attributes in a multi‑level inheritance chain (`Person -> Employee -> Manager`).
3. Write an abstract class `FileHandler` with `read()` and `write()` abstract methods. Implement `TextFileHandler` and `BinaryFileHandler`.
4. Demonstrate polymorphism by creating a list of different `Shape` objects and calling `area()` on each.
5. Refactor an existing inheritance‑heavy code to use composition (e.g., `Printer` has a `Scanner` instead of inheriting from `Machine`).
This lesson provides **6 complete examples** covering all key advanced OOP concepts in Python.
# Object-Oriented Programming - Part 2
print("OBJECT-ORIENTED PROGRAMMING (OOP) - PART 2")
print("=" * 60)
# Example 1: Basic Inheritance
print("\n1. BASIC INHERITANCE")
print("-" * 30)
class Animal:
"""Base class for all animals"""
def __init__(self, name, age):
self.name = name
self.age = age
def eat(self):
return f"{self.name} is eating."
def sleep(self):
return f"{self.name} is sleeping."
def make_sound(self):
return "Some generic animal sound"
class Dog(Animal): # Dog inherits from Animal
"""Dog class - extends Animal"""
def __init__(self, name, age, breed):
# Call parent class constructor
super().__init__(name, age)
self.breed = breed
# Override parent method
def make_sound(self):
return "Woof! Woof!"
# Add new method specific to Dog
def fetch(self):
return f"{self.name} is fetching the ball!"
class Cat(Animal): # Cat inherits from Animal
"""Cat class - extends Animal"""
def __init__(self, name, age, color):
super().__init__(name, age)
self.color = color
# Override parent method
def make_sound(self):
return "Meow!"
# Add new method specific to Cat
def climb(self):
return f"{self.name} is climbing a tree!"
# Create animals
generic_animal = Animal("Generic", 1)
dog = Dog("Buddy", 3, "Golden Retriever")
cat = Cat("Whiskers", 2, "Orange")
print("Animal behaviors:")
print(f"Generic: {generic_animal.make_sound()}")
print(f"Dog: {dog.make_sound()}")
print(f"Cat: {cat.make_sound()}")
print("\nCommon behaviors (inherited):")
print(dog.eat())
print(cat.sleep())
print("\nSpecial behaviors (specific to subclass):")
print(dog.fetch())
print(cat.climb())
print("\nAnimal information:")
print(f"Dog: {dog.name}, {dog.age} years old, {dog.breed}")
print(f"Cat: {cat.name}, {cat.age} years old, {cat.color}")
# Example 2: Multi-level Inheritance
print("\n\n2. MULTI-LEVEL INHERITANCE")
print("-" * 30)
class Vehicle:
"""Base class for all vehicles"""
def __init__(self, brand, model, year):
self.brand = brand
self.model = model
self.year = year
self.speed = 0
def start(self):
return f"{self.brand} {self.model} started."
def stop(self):
self.speed = 0
return f"{self.brand} {self.model} stopped."
def accelerate(self, increment):
self.speed += increment
return f"Accelerating to {self.speed} km/h"
class Car(Vehicle):
"""Car class - extends Vehicle"""
def __init__(self, brand, model, year, doors):
super().__init__(brand, model, year)
self.doors = doors
def honk(self):
return "Beep! Beep!"
class ElectricCar(Car):
"""Electric Car - extends Car"""
def __init__(self, brand, model, year, doors, battery_capacity):
super().__init__(brand, model, year, doors)
self.battery_capacity = battery_capacity
self.battery_level = 100
def charge(self, amount):
"""Charge the battery"""
self.battery_level = min(100, self.battery_level + amount)
return f"Battery: {self.battery_level}%"
def drive(self, distance):
"""Drive and consume battery"""
battery_used = distance * 0.5 # 0.5% per km
if battery_used <= self.battery_level:
self.battery_level -= battery_used
return f"Drove {distance}km. Battery: {self.battery_level:.1f}%"
return "Not enough battery!"
# Create vehicles
regular_car = Car("Toyota", "Camry", 2022, 4)
tesla = ElectricCar("Tesla", "Model 3", 2023, 4, 75)
print("Regular Car:")
print(regular_car.start())
print(regular_car.accelerate(50))
print(regular_car.honk())
print("\nElectric Car:")
print(tesla.start())
print(tesla.drive(50))
print(tesla.charge(25))
# Check inheritance
print("\nChecking inheritance hierarchy:")
print(f"Is tesla an ElectricCar? {isinstance(tesla, ElectricCar)}")
print(f"Is tesla a Car? {isinstance(tesla, Car)}")
print(f"Is tesla a Vehicle? {isinstance(tesla, Vehicle)}")
print(f"Is ElectricCar a subclass of Vehicle? {issubclass(ElectricCar, Vehicle)}")
# Example 3: Multiple Inheritance
print("\n\n3. MULTIPLE INHERITANCE")
print("-" * 30)
class Camera:
"""Camera functionality"""
def __init__(self, resolution):
self.resolution = resolution
def take_photo(self):
return f"Taking photo at {self.resolution} resolution"
def zoom(self, factor):
return f"Zooming {factor}x"
class Phone:
"""Phone functionality"""
def __init__(self, number):
self.number = number
def call(self, contact):
return f"Calling {contact} from {self.number}"
def send_sms(self, contact, message):
return f"Sending SMS to {contact}: {message}"
class SmartPhone(Camera, Phone): # Inherits from both
"""SmartPhone - combines Camera and Phone"""
def __init__(self, number, resolution, model):
# Initialize both parent classes
Camera.__init__(self, resolution)
Phone.__init__(self, number)
self.model = model
def browse_internet(self):
return f"Browsing internet on {self.model}"
# Create smartphone
iphone = SmartPhone("123-456-7890", "12MP", "iPhone 15")
print("Smartphone features:")
print(iphone.take_photo())
print(iphone.zoom(2))
print(iphone.call("Mom"))
print(iphone.send_sms("Friend", "Hello!"))
print(iphone.browse_internet())
# Check Method Resolution Order (MRO)
print("\nMethod Resolution Order:")
for i, cls in enumerate(SmartPhone.__mro__, 1):
print(f"{i}. {cls.__name__}")
# Example 4: Abstract Base Classes
print("\n\n4. ABSTRACT BASE CLASSES")
print("-" * 30)
from abc import ABC, abstractmethod
class Shape(ABC): # Abstract Base Class
"""Abstract base class for shapes"""
@abstractmethod
def area(self):
"""Calculate area - must be implemented by subclasses"""
pass
@abstractmethod
def perimeter(self):
"""Calculate perimeter - must be implemented by subclasses"""
pass
def display_info(self):
"""Common method for all shapes"""
return f"Area: {self.area():.2f}, Perimeter: {self.perimeter():.2f}"
class Circle(Shape):
"""Circle class implementing Shape"""
def __init__(self, radius):
self.radius = radius
def area(self):
import math
return math.pi * self.radius ** 2
def perimeter(self):
import math
return 2 * math.pi * self.radius
class Rectangle(Shape):
"""Rectangle class implementing Shape"""
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
def perimeter(self):
return 2 * (self.length + self.width)
# Cannot instantiate abstract class
# shape = Shape() # This would raise TypeError
# Create concrete shapes
circle = Circle(5)
rectangle = Rectangle(4, 6)
print("Shape calculations:")
print(f"Circle with radius 5:")
print(circle.display_info())
print(f"\nRectangle 4x6:")
print(rectangle.display_info())
# Example 5: Polymorphism in action
print("\n\n5. POLYMORPHISM IN ACTION")
print("-" * 30)
class Employee(ABC):
"""Abstract base employee class"""
def __init__(self, name, employee_id):
self.name = name
self.employee_id = employee_id
@abstractmethod
def calculate_salary(self):
pass
def get_info(self):
return f"{self.name} (ID: {self.employee_id})"
class FullTimeEmployee(Employee):
"""Full-time employee with monthly salary"""
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):
"""Part-time employee with hourly wage"""
def __init__(self, name, employee_id, hourly_wage, hours_worked):
super().__init__(name, employee_id)
self.hourly_wage = hourly_wage
self.hours_worked = hours_worked
def calculate_salary(self):
return self.hourly_wage * self.hours_worked
class Contractor(Employee):
"""Contractor with project-based payment"""
def __init__(self, name, employee_id, project_fee, completed_projects):
super().__init__(name, employee_id)
self.project_fee = project_fee
self.completed_projects = completed_projects
def calculate_salary(self):
return self.project_fee * self.completed_projects
# Create different types of employees
employees = [
FullTimeEmployee("Alice", "FT001", 5000),
PartTimeEmployee("Bob", "PT001", 20, 80),
Contractor("Charlie", "CT001", 1000, 3)
]
# Process all employees polymorphically
print("Employee Salaries:")
print("-" * 40)
total_payroll = 0
for employee in employees:
salary = employee.calculate_salary()
total_payroll += salary
print(f"{employee.get_info():30} Salary: ${salary:,.2f}")
print("-" * 40)
print(f"Total Payroll:{'':22} ${total_payroll:,.2f}")
# Example 6: Composition vs Inheritance
print("\n\n6. COMPOSITION VS INHERITANCE")
print("-" * 30)
# Composition example
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
def start(self):
return "Engine started"
def stop(self):
return "Engine stopped"
class Wheels:
def __init__(self, count):
self.count = count
def rotate(self):
return f"{self.count} wheels rotating"
# Using composition (has-a relationship)
class CarComposition:
def __init__(self, brand, engine_hp, wheel_count):
self.brand = brand
self.engine = Engine(engine_hp) # Car HAS an Engine
self.wheels = Wheels(wheel_count) # Car HAS Wheels
def drive(self):
return f"{self.brand}: {self.engine.start()}, {self.wheels.rotate()}"
# Create car using composition
my_car = CarComposition("Toyota", 150, 4)
print("Car using composition:")
print(my_car.drive())
print(f"Engine HP: {my_car.engine.horsepower}")
print(f"Wheel count: {my_car.wheels.count}")
print("\nWhen to use each:")
print("Inheritance (is-a): A Dog IS AN Animal")
print("Composition (has-a): A Car HAS AN Engine")
→ Run this code interactively