- Understand inheritance and how classes inherit from each other
- Override methods in child classes
- Use super() to call parent methods
- Understand polymorphism and method overriding
Inheritance and Polymorphism
Imagine you're building a family tree. Your grandparents pass down traits to your parents, who pass them down to you. You inherit some traits but also develop your own unique characteristics.
In programming, inheritance works the same way. A child class can inherit properties and methods from a parent class, but can also add its own unique features or modify inherited ones. This creates a powerful hierarchy of related classes.
Basic Inheritance
Creating a Child Class
class Animal:
"""Parent class for all animals."""
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 inherits from Animal
class Dog(Animal):
"""Dog inherits from Animal."""
def __init__(self, name, breed):
# Call parent constructor
super().__init__(name, "Dog")
# Add dog-specific attribute
self.breed = breed
# Override parent method
def make_sound(self):
return "Woof!"
# Add dog-specific method
def fetch(self, item):
return f"{self.name} fetches the {item}"
Using Inheritance
# Create instances
generic_animal = Animal("Generic", "Unknown")
buddy = Dog("Buddy", "Golden Retriever")
print(generic_animal.make_sound()) # "Some generic animal sound"
print(buddy.make_sound()) # "Woof!" (overridden)
print(buddy.eat("bones")) # "Buddy eats bones" (inherited)
print(buddy.fetch("ball")) # "Buddy fetches the ball" (new method)
# Check inheritance
print(isinstance(buddy, Dog)) # True
print(isinstance(buddy, Animal)) # True - Dog IS-A Animal
print(isinstance(generic_animal, Dog)) # False
Method Overriding
Child classes can provide their own implementation of inherited methods:
class Bird(Animal):
def __init__(self, name, can_fly=True):
super().__init__(name, "Bird")
self.can_fly = can_fly
def make_sound(self):
return "Tweet!"
def fly(self):
if self.can_fly:
return f"{self.name} flies through the sky!"
return f"{self.name} cannot fly."
class Penguin(Bird):
def __init__(self, name):
super().__init__(name, can_fly=False) # Penguins can't fly
def make_sound(self):
return "Honk!"
def swim(self):
return f"{self.name} swims gracefully in the water!"
The super() Function
super() allows you to call methods from the parent class:
class Cat(Animal):
def __init__(self, name, lives=9):
super().__init__(name, "Cat") # Call parent __init__
self.lives = lives
def make_sound(self):
parent_sound = super().make_sound() # Get parent method result
return f"Meow! (Also {parent_sound})"
def lose_life(self):
self.lives -= 1
if self.lives <= 0:
return f"{self.name} has no lives left!"
return f"{self.name} has {self.lives} lives remaining."
Polymorphism
Polymorphism means "many forms." It allows different objects to respond to the same method call in their own way:
# Different animals respond differently to make_sound()
animals = [
Dog("Buddy", "Golden Retriever"),
Bird("Tweety", True),
Cat("Whiskers"),
Animal("Generic", "Unknown")
]
for animal in animals:
print(f"{animal.name}: {animal.make_sound()}")
# Output:
# Buddy: Woof.
# Tweety: Tweet.
# Whiskers: Meow! (Also Some generic animal sound)
# Generic: Some generic animal sound
Duck Typing
Python uses "duck typing" - if it walks like a duck and quacks like a duck, it's a duck:
def animal_sounds(animals):
"""Works with any object that has a make_sound method."""
for animal in animals:
print(animal.make_sound())
# This works with any object that has make_sound()
class ToyDog:
def make_sound(self):
return "Squeak!"
animal_sounds([
Dog("Buddy", "Golden Retriever"),
ToyDog() # Not an Animal, but has make_sound()
])
Shape Hierarchy Example
class Shape:
"""Base class for all shapes."""
def __init__(self, name):
self.name = name
def area(self):
"""Calculate area - to be overridden by subclasses."""
raise NotImplementedError("Subclasses must implement area()")
def perimeter(self):
"""Calculate perimeter - to be overridden by subclasses."""
raise NotImplementedError("Subclasses must implement perimeter()")
def describe(self):
return f"I am a {self.name}"
class Rectangle(Shape):
def __init__(self, width, height):
super().__init__("Rectangle")
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):
super().__init__("Circle")
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
def perimeter(self):
return 2 * 3.14159 * self.radius
class Triangle(Shape):
def __init__(self, base, height, side1, side2, side3):
super().__init__("Triangle")
self.base = base
self.height = height
self.sides = [side1, side2, side3]
def area(self):
return 0.5 * self.base * self.height
def perimeter(self):
return sum(self.sides)
Using the Shape Hierarchy
shapes = [
Rectangle(4, 5),
Circle(3),
Triangle(6, 4, 3, 4, 5)
]
for shape in shapes:
print(f"{shape.describe()}")
print(f" Area: {shape.area():.2f}")
print(f" Perimeter: {shape.perimeter():.2f}")
print()
# All shapes respond to the same methods but behave differently.
Multiple Inheritance
Python supports multiple inheritance (inheriting from multiple parents):
class Flyable:
def fly(self):
return "I can fly!"
class Swimmable:
def swim(self):
return "I can swim!"
class Duck(Flyable, Swimmable, Animal):
def __init__(self, name):
super().__init__(name, "Duck")
def make_sound(self):
return "Quack!"
# Duck inherits from Animal, Flyable, and Swimmable
duck = Duck("Donald")
print(duck.make_sound()) # "Quack!" (from Duck)
print(duck.fly()) # "I can fly!" (from Flyable)
print(duck.swim()) # "I can swim!" (from Swimmable)
print(duck.eat("bread")) # "Donald eats bread" (from Animal)
Common Inheritance Patterns
1. Abstract Base Classes
from abc import ABC, abstractmethod
class Vehicle(ABC):
@abstractmethod
def move(self):
pass
@abstractmethod
def stop(self):
pass
class Car(Vehicle):
def move(self):
return "Car drives on road"
def stop(self):
return "Car applies brakes"
# vehicle = Vehicle() # Error! Can't instantiate abstract class
car = Car() # OK
2. Method Resolution Order (MRO)
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
print(D.__mro__) # (<class '__main__.D'>, <class '__main__.B'>,
# <class '__main__.C'>, <class '__main__.A'>,
# <class 'object'>)
Key Points to Remember
- Inheritance = Child classes inherit from parents using
class Child(Parent): - Method Overriding = Child provides different implementation with same method name
- super() = Call parent class methods like
super().__init__()orsuper().method_name() - Polymorphism = Same method, different behavior; objects respond differently to same method call
- Duck Typing = If it quacks like a duck, it's a duck; behavior matters more than inheritance
Inheritance lets classes build upon each other, but how do we protect our object's data? In the next lesson, we'll explore encapsulation and abstraction – the art of hiding complexity and protecting data integrity.
