""" ================================================================================ File: 03_inheritance.py Topic: Inheritance in Python ================================================================================ This file demonstrates inheritance in Python, a fundamental OOP concept that allows classes to inherit attributes and methods from other classes. This promotes code reuse and establishes relationships between classes. Key Concepts: - Single inheritance - Method overriding - super() function - Multiple inheritance - Method Resolution Order (MRO) - Abstract base classes ================================================================================ """ # ----------------------------------------------------------------------------- # 1. Basic Inheritance # ----------------------------------------------------------------------------- # Child class inherits from parent class print("--- Basic Inheritance ---") class Animal: """Base class for all animals.""" def __init__(self, name): """Initialize animal with a name.""" self.name = name def speak(self): """Make a generic sound.""" return "Some generic sound" def describe(self): """Describe the animal.""" return f"I am {self.name}" class Dog(Animal): """Dog class inheriting from Animal.""" pass # Inherits everything from Animal # Dog has all Animal's methods buddy = Dog("Buddy") print(buddy.describe()) print(f"Says: {buddy.speak()}") print(f"Dog is instance of Animal: {isinstance(buddy, Animal)}") # ----------------------------------------------------------------------------- # 2. Method Overriding # ----------------------------------------------------------------------------- # Child class can override parent methods print("\n--- Method Overriding ---") class Cat(Animal): """Cat class with overridden method.""" def speak(self): """Cats meow instead of generic sound.""" return "Meow!" class Cow(Animal): """Cow class with overridden method.""" def speak(self): """Cows moo.""" return "Moo!" # Each animal speaks differently animals = [Dog("Rex"), Cat("Whiskers"), Cow("Bessie")] print("Each animal speaks:") for animal in animals: print(f" {animal.name}: {animal.speak()}") # ----------------------------------------------------------------------------- # 3. Extending Parent Class with super() # ----------------------------------------------------------------------------- # Use super() to call parent methods print("\n--- Using super() ---") class Bird(Animal): """Bird class extending Animal.""" def __init__(self, name, can_fly=True): """Initialize bird with flying ability.""" super().__init__(name) # Call parent's __init__ self.can_fly = can_fly def speak(self): """Birds chirp.""" return "Chirp!" def describe(self): """Extend parent's describe method.""" base = super().describe() # Get parent's description fly_status = "can fly" if self.can_fly else "cannot fly" return f"{base} and I {fly_status}" sparrow = Bird("Sparrow", can_fly=True) penguin = Bird("Penny", can_fly=False) print(sparrow.describe()) print(penguin.describe()) # ----------------------------------------------------------------------------- # 4. Inheritance Chain # ----------------------------------------------------------------------------- # Classes can inherit from other child classes print("\n--- Inheritance Chain ---") class Vehicle: """Base vehicle class.""" def __init__(self, brand): self.brand = brand def start(self): return "Vehicle starting..." class Car(Vehicle): """Car inherits from Vehicle.""" def __init__(self, brand, model): super().__init__(brand) self.model = model def start(self): return f"{self.brand} {self.model} engine starting..." class ElectricCar(Car): """ElectricCar inherits from Car.""" def __init__(self, brand, model, battery_kw): super().__init__(brand, model) self.battery_kw = battery_kw def start(self): return f"{self.brand} {self.model} powering up silently... (Battery: {self.battery_kw}kW)" def charge(self): return f"Charging {self.battery_kw}kW battery..." # Create instances regular_car = Car("Toyota", "Camry") electric_car = ElectricCar("Tesla", "Model 3", 75) print(regular_car.start()) print(electric_car.start()) print(electric_car.charge()) # Check inheritance print(f"\nElectricCar is Car: {isinstance(electric_car, Car)}") print(f"ElectricCar is Vehicle: {isinstance(electric_car, Vehicle)}") # ----------------------------------------------------------------------------- # 5. Multiple Inheritance # ----------------------------------------------------------------------------- # A class can inherit from multiple parent classes print("\n--- Multiple Inheritance ---") class Flyable: """Mixin class for flying ability.""" def fly(self): return f"{self.name} is flying!" def land(self): return f"{self.name} has landed." class Swimmable: """Mixin class for swimming ability.""" def swim(self): return f"{self.name} is swimming!" def dive(self): return f"{self.name} dives underwater." class Duck(Animal, Flyable, Swimmable): """Duck can do everything!""" def speak(self): return "Quack!" # Duck has methods from all parent classes duck = Duck("Donald") print(f"{duck.name} says: {duck.speak()}") print(duck.fly()) print(duck.swim()) print(duck.describe()) # ----------------------------------------------------------------------------- # 6. Method Resolution Order (MRO) # ----------------------------------------------------------------------------- # Python determines method lookup order print("\n--- Method Resolution Order (MRO) ---") class A: def method(self): return "A" class B(A): def method(self): return "B" class C(A): def method(self): return "C" class D(B, C): pass class E(B, C): def method(self): return "E" # Check MRO print("MRO for class D:") for cls in D.__mro__: print(f" {cls.__name__}") d = D() e = E() print(f"\nd.method(): {d.method()}") # Returns "B" (first in MRO after D) print(f"e.method(): {e.method()}") # Returns "E" (overridden) # ----------------------------------------------------------------------------- # 7. Calling Methods from Specific Parent # ----------------------------------------------------------------------------- print("\n--- Calling Specific Parent Methods ---") class Parent1: def greet(self): return "Hello from Parent1" class Parent2: def greet(self): return "Hello from Parent2" class Child(Parent1, Parent2): def greet(self): return "Hello from Child" def greet_all(self): """Call all parent greet methods.""" return [ f"Child: {self.greet()}", f"Parent1: {Parent1.greet(self)}", f"Parent2: {Parent2.greet(self)}" ] child = Child() print("Calling all greet methods:") for greeting in child.greet_all(): print(f" {greeting}") # ----------------------------------------------------------------------------- # 8. Abstract Base Classes # ----------------------------------------------------------------------------- # Define interfaces that must be implemented print("\n--- Abstract Base Classes ---") from abc import ABC, abstractmethod class Shape(ABC): """Abstract base class for shapes.""" @abstractmethod def area(self): """Calculate area - must be implemented.""" pass @abstractmethod def perimeter(self): """Calculate perimeter - must be implemented.""" pass def describe(self): """Non-abstract method - can be used as-is.""" return f"A shape with area {self.area():.2f}" class Rectangle(Shape): """Concrete rectangle class.""" 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): """Concrete circle class.""" 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 # Cannot instantiate abstract class # shape = Shape() # TypeError! # Concrete classes work fine rect = Rectangle(5, 3) circle = Circle(4) print(f"Rectangle: area={rect.area()}, perimeter={rect.perimeter()}") print(f"Circle: area={circle.area():.2f}, perimeter={circle.perimeter():.2f}") print(rect.describe()) # ----------------------------------------------------------------------------- # 9. Using isinstance() and issubclass() # ----------------------------------------------------------------------------- print("\n--- Type Checking ---") class Mammal(Animal): pass class Reptile(Animal): pass class DogV2(Mammal): def speak(self): return "Woof!" class Snake(Reptile): def speak(self): return "Hiss!" dog = DogV2("Rex") snake = Snake("Slinky") # isinstance() checks if object is instance of class print("isinstance checks:") print(f" dog isinstance of DogV2: {isinstance(dog, DogV2)}") print(f" dog isinstance of Mammal: {isinstance(dog, Mammal)}") print(f" dog isinstance of Animal: {isinstance(dog, Animal)}") print(f" dog isinstance of Reptile: {isinstance(dog, Reptile)}") # issubclass() checks class relationships print("\nissubclass checks:") print(f" DogV2 issubclass of Mammal: {issubclass(DogV2, Mammal)}") print(f" DogV2 issubclass of Animal: {issubclass(DogV2, Animal)}") print(f" DogV2 issubclass of Reptile: {issubclass(DogV2, Reptile)}") # ----------------------------------------------------------------------------- # 10. Practical Example: Employee Hierarchy # ----------------------------------------------------------------------------- print("\n--- Practical Example: Employee Hierarchy ---") class Employee: """Base employee class.""" def __init__(self, name, employee_id, salary): self.name = name self.employee_id = employee_id self._salary = salary @property def salary(self): return self._salary def get_annual_salary(self): return self._salary * 12 def __str__(self): return f"{self.name} (ID: {self.employee_id})" class Manager(Employee): """Manager with team and bonus.""" def __init__(self, name, employee_id, salary, team_size=0): super().__init__(name, employee_id, salary) self.team_size = team_size def get_annual_salary(self): # Managers get bonus based on team size base = super().get_annual_salary() bonus = self.team_size * 1000 # $1000 per team member return base + bonus def __str__(self): return f"Manager {super().__str__()} - Team: {self.team_size}" class Developer(Employee): """Developer with tech stack.""" def __init__(self, name, employee_id, salary, languages): super().__init__(name, employee_id, salary) self.languages = languages def get_annual_salary(self): # Developers get extra for each language base = super().get_annual_salary() language_bonus = len(self.languages) * 500 # $500 per language return base + language_bonus def __str__(self): langs = ", ".join(self.languages) return f"Developer {super().__str__()} - Languages: {langs}" # Create employees manager = Manager("Alice", "M001", 8000, team_size=5) dev1 = Developer("Bob", "D001", 6000, ["Python", "JavaScript", "SQL"]) dev2 = Developer("Charlie", "D002", 5500, ["Python"]) employees = [manager, dev1, dev2] print("Employee Details:") for emp in employees: print(f" {emp}") print(f" Annual Salary: ${emp.get_annual_salary():,}")