Lecture 09: Expanding Your OOP Skills - Inheritance and Working with Many Objects
Welcome to Lecture 09! In our last lecture, we took our first exciting steps into the world of Object-Oriented Programming (OOP) by learning about basic classes and objects. We saw how a class acts as a blueprint (like for a Student) and how we can create individual instances (objects like student_priya or student_rohan) with their own attributes (like name, student_id) and methods (actions like enroll_in_course()).
Today’s Goals:
We’re going to build upon that foundation and explore one of the most powerful concepts in OOP: Inheritance.
- Understand Inheritance: Learn how we can create new classes (child classes or subclasses) that inherit properties and behaviors from existing classes (parent classes or superclasses). This promotes code reuse and helps create logical hierarchies.
- Master Key Inheritance Techniques: We’ll see how to use
super().__init__()to call the parent’s constructor, how to override parent methods, and how to add new, specialized attributes and methods to child classes. - Work with Collections of Objects: We’ll learn how to manage lists containing objects of different but related classes and see a glimpse of polymorphism.
- Project Theme: We’ll use a fun “Pet Shop” or “Digital Zoo” theme to illustrate these concepts by creating a hierarchy of
Petclasses.
This lecture will significantly enhance your ability to model more complex relationships and build more organized and extensible Python programs.
Recap of Basic Classes (from Lecture 08)
Just a very quick reminder of what a class definition looked like:
class MyClass:
def __init__(self, param1, param2): # Constructor
self.attribute1 = param1 # Instance attribute
self.attribute2 = param2 # Instance attribute
def my_method(self, extra_param): # An instance method
# 'self' refers to the specific object this method is called on
print(f"Attribute1 is {self.attribute1}")
# ... code that uses self.attribute1, self.attribute2, and extra_param ...
return self.attribute2 + extra_param
class MyClass:defines the blueprint.__init__(self, ...)initializes a new object’s attributes.selfis the instance itself.self.attribute_name = ...creates instance-specific data.- Other functions defined inside the class are methods that objects of this class can perform.
Class Attributes vs. Instance Attributes (A Quick Distinction)
Before diving into inheritance, let’s clarify a useful distinction:
-
Instance Attributes: These are the most common type we’ve seen. They are specific to each instance (object) of a class. They are defined inside methods (usually
__init__) usingself.attribute_name = value.- Example: If
student_priya = Student("Priya", ...)andstudent_rohan = Student("Rohan", ...), thenstudent_priya.name(“Priya”) is distinct fromstudent_rohan.name(“Rohan”).
- Example: If
-
Class Attributes: These are attributes that are shared by all instances of a class. They belong to the class itself, not just to one specific object. You define them directly under the
classline, outside of any method.class Pet: # This is a CLASS attribute - shared by all Pet instances is_domesticated = True species_description = "Animal Companion" def __init__(self, name): # This is an INSTANCE attribute - specific to each pet self.name = name pet1 = Pet("Raja") # Changed from Buddy pet2 = Pet("Bella") # Changed from Misty print(f"{pet1.name} is domesticated: {pet1.is_domesticated}") print(f"{pet2.name} is domesticated: {pet2.is_domesticated}") print(f"All Pets are generally: {Pet.species_description}") # You can change a class attribute via the class name # Pet.is_domesticated = FalseWhile useful in some scenarios (like defining constants related to a class or default values), we’ll mostly focus on instance attributes for now. Understanding instance attributes is key for inheritance.
Understanding Inheritance (“Is-A” Relationship)
What is Inheritance?
Inheritance is a fundamental concept in Object-Oriented Programming that allows you to create a new class (called a subclass or child class) that inherits attributes and methods from an existing class (called a superclass or parent class).
Think of it as creating a specialized version of a more general concept. The child class starts with all the features of the parent class and can then add its own unique features or modify (override) some of the inherited ones.
The “Is-A” Relationship:
Inheritance typically models an “Is-A” relationship.
- A
Dogis a type ofPet. - A
Catis a type ofPet. - A
Sparrowis a type ofBird. - A
Birdis a type ofAnimal. - A
Caris a type ofVehicle.
If you can say “ChildClass is a ParentClass”, then inheritance might be an appropriate way to model that relationship in your code.
Benefits of Inheritance:
- Code Reusability: Instead of writing the same code (for common attributes and methods) in multiple classes, you define it once in the parent class, and all child classes automatically get it. This is DRY (Don’t Repeat Yourself) at a class level!
- Organization and Structure: Inheritance helps create a logical hierarchy of classes, making your code easier to understand and navigate. It reflects real-world relationships between concepts.
- Extensibility: You can easily create new classes that extend the functionality of existing classes without modifying the original parent class.
- Polymorphism (Many Forms): This is a powerful concept (which we’ll touch upon) where you can treat objects of different child classes as if they are objects of the parent class, but they still behave in their specialized ways.
Defining a Base Class (Parent Class)
Let’s start our “Pet Shop/Digital Zoo” project by defining a general base class called Pet. This class will contain attributes and methods common to all pets.
# base_pet.py (or directly in your main script for now)
class Pet:
# Optional class attribute - a characteristic shared by all pets (conceptually)
is_alive = True
def __init__(self, name, age, sound="a generic sound"):
"""
Constructor for the Pet class.
Initializes common attributes for all pets.
"""
self.name = name # Instance attribute for the pet's name
self.age = age # Instance attribute for the pet's age (in years)
self.sound = sound # Instance attribute for the sound the pet makes
print(f"Pet object '{self.name}' created.")
def make_sound(self):
"""Makes the pet produce its characteristic sound."""
print(f"{self.name} says: {self.sound}")
def show_details(self):
"""Displays the basic details of the pet."""
print(f"\n--- Details for {self.name} ---")
print(f" Name: {self.name}")
print(f" Age: {self.age} years old")
# We could also print the class attribute here:
# print(f" Is alive? {Pet.is_alive}")
def eat(self, food="pet food"): # Adding another common behavior
print(f"{self.name} is eating {food}.")
# Example of using the Pet class directly (before creating subclasses)
# generic_pet = Pet("Creature", 2)
# generic_pet.show_details()
# generic_pet.make_sound()
# generic_pet.eat("seeds")
Explanation:
class Pet:: Defines our base class.is_alive = True: An optional class attribute. Every pet created from this class (or its children) will initially share this characteristic.__init__(self, name, age, sound="a generic sound"):- The constructor takes
nameandageas required arguments. soundis an optional argument with a default value “a generic sound”. This means if we don’t provide a sound when creating aPet, it will use this default.self.name,self.age,self.soundare instance attributes, unique to each pet object.
- The constructor takes
make_sound(self): A method that prints the sound of the pet.show_details(self): A method to print the pet’s name and age.eat(self, food="pet food"): Another common method for all pets.
Creating Subclasses (Child Classes)
Now, let’s create more specific types of pets that inherit from our Pet class. We’ll create Dog and Cat classes.
Syntax for Inheritance:
To make a class inherit from a parent class, you put the parent class’s name in parentheses after the child class’s name:
class ChildClassName(ParentClassName):
The Dog Subclass
class Dog(Pet): # Dog inherits from Pet
def __init__(self, name, age, sound, breed):
# Call the __init__ method of the parent class (Pet)
# to initialize common attributes like name, age, sound.
super().__init__(name, age, sound)
# Now, add attributes specific to Dog
self.breed = breed # Dog-specific instance attribute
print(f"Dog object '{self.name}' of breed '{self.breed}' created.")
# Method Overriding: Dog provides its own version of make_sound
def make_sound(self):
# We could call the parent's method if we wanted to extend it:
# super().make_sound() # This would print "Raja says: Woof woof!" if sound was "Woof woof!"
print(f"{self.name} (a {self.breed} dog) barks: Woof! Woof!")
# Adding a new method specific to Dog
def fetch(self, item="ball"):
print(f"{self.name} enthusiastically fetches the {item}!")
# Overriding show_details to add breed information
def show_details(self):
super().show_details() # Call the parent's show_details to print name and age
print(f" Breed: {self.breed}") # Add Dog-specific detail
Explanation of Dog(Pet):
class Dog(Pet):: This line declares thatDogis a child class ofPet.Dognow automatically has all attributes (likeis_alive) and methods (likeeat()) thatPethas.super().__init__(name, age, sound)(Thesuper()Call):- Inside the
Dog’s__init__method,super()is a special function that gives you a reference to the parent class (Pet). super().__init__(name, age, sound)explicitly calls the__init__method of thePetclass. This is crucial for ensuring that the common attributes (name,age,sound) defined in the parent are properly initialized for theDogobject.- You pass the necessary arguments (
name,age,sound) that the parent’s__init__method expects.
- Inside the
self.breed = breed: After the parent’s__init__has done its job, we add attributes that are specific only toDogobjects, likebreed.- Method Overriding (
make_sound):- The
Dogclass defines its ownmake_sound(self)method. Since this method has the same name and parameters (justself) as a method in its parent class (Pet), it overrides the parent’s version. - When you call
make_sound()on aDogobject, Python will execute this specializedDogversion, not the genericPetversion.
- The
- Adding New Methods (
fetch):- The
Dogclass can have methods that don’t exist in thePetclass at all, likefetch(self, item). This method is specific to dogs.
- The
- Overriding and Extending (
show_details):- The
Dogclass overridesshow_details. super().show_details(): Inside theDog’sshow_details, we first call the parent’sshow_detailsmethod usingsuper(). This reuses the parent’s logic to print the name and age.- Then, we add
print(f" Breed: {self.breed}")to display information specific to dogs. This is a common pattern: reuse what the parent does, then add more specialized behavior.
- The
The Cat Subclass
Let’s create a Cat class similarly:
class Cat(Pet): # Cat also inherits from Pet
def __init__(self, name, age, sound, fur_color):
super().__init__(name, age, sound) # Initialize common Pet attributes
self.fur_color = fur_color # Cat-specific attribute
print(f"Cat object '{self.name}' with {self.fur_color} fur created.")
# Method Overriding for Cat
def make_sound(self):
print(f"{self.name} (a {self.fur_color} cat) purrs: Meooow... Purrrr...")
# New method specific to Cat
def chase_laser_pointer(self):
print(f"{self.name} excitedly chases the red dot!")
# Overriding show_details to add fur color
def show_details(self):
super().show_details() # Call parent's version first
print(f" Fur Color: {self.fur_color}")
The Cat class follows the same principles:
- It inherits from
Pet. - Its
__init__callssuper().__init__and addsself.fur_color. - It overrides
make_sound()with a cat-specific sound. - It adds a new method
chase_laser_pointer(). - It overrides and extends
show_details().
Example Hierarchy Usage
Now, let’s create instances of our specific pet types and see them in action!
# Create a Dog object
my_dog_raja = Dog(name="Raja", age=3, sound="Loud Bark!", breed="Golden Retriever") # Changed Buddy to Raja
# Create a Cat object
my_cat_bella = Cat(name="Bella", age=5, sound="Quiet Purr", fur_color="Grey Tabby") # Changed Misty to Bella
# Create a generic Pet object (perhaps a bird)
my_bird_tweety = Pet(name="Tweety", age=1, sound="Chirp Chirp")
print("\n--- Interacting with our Pets ---")
# Calling overridden make_sound method
my_dog_raja.make_sound()
my_cat_bella.make_sound()
my_bird_tweety.make_sound()
# Calling overridden show_details method
my_dog_raja.show_details()
my_cat_bella.show_details()
my_bird_tweety.show_details()
# Calling methods specific to subclasses
my_dog_raja.fetch("squeaky toy")
my_cat_bella.chase_laser_pointer()
# Calling a method inherited from the Pet class (available to all)
my_dog_raja.eat("dog food")
my_cat_bella.eat("fish treats")
my_bird_tweety.eat("sunflower seeds")
# Accessing the class attribute (shared by all)
print(f"\nIs Raja alive? {my_dog_raja.is_alive}")
print(f"Is Bella alive? {Cat.is_alive}")
This demonstrates how child classes inherit functionality but can also provide their own specialized versions or entirely new behaviors.
Working with a List of Objects (Polymorphism Introduction)
One of the powerful aspects of inheritance is that you can treat objects of different child classes (that share a common parent) in a uniform way, yet they will still behave according to their specific type. This concept is called polymorphism (which means “many forms”).
Let’s create a list containing different types of pets:
all_pets_in_shop = [
Dog(name="Sheru", age=4, sound="Deep Woof", breed="Labrador"),
Cat(name="Rani", age=2, sound="Gentle Meow", fur_color="White Persian"),
Pet(name="Polly", age=1, sound="Squawk! Polly want a cracker!"),
Dog(name="Veeru", age=6, sound="Yip yip!", breed="Pomeranian") # Changed Tommy to Veeru
]
print("\n\n--- Pet Shop Daily Roll Call ---")
for current_pet_object in all_pets_in_shop:
print("\nNext Pet:")
current_pet_object.show_details()
current_pet_object.make_sound()
current_pet_object.eat()
if isinstance(current_pet_object, Dog):
current_pet_object.fetch("newspaper")
elif isinstance(current_pet_object, Cat):
current_pet_object.chase_laser_pointer()
Explanation of Polymorphism in Action:
all_pets_in_shopis a list containing objects of different types (Dog,Cat,Pet).- When we loop
for current_pet_object in all_pets_in_shop::current_pet_object.show_details(): Python is smart! Ifcurrent_pet_objectis aDog, it callsDog.show_details(). If it’s aCat, it callsCat.show_details(). If it’s just aPet, it callsPet.show_details().current_pet_object.make_sound(): Similarly, the correctmake_soundmethod for the actual class of the object is executed.
isinstance(object, ClassName): This built-in function checks if anobjectis an instance of a particularClassName(or an instance of any subclass ofClassName). We use it here to safely call methods likefetch()only onDogobjects, becauseCator genericPetobjects don’t have afetch()method.
This ability to treat different objects uniformly while still getting their specialized behavior is a cornerstone of flexible and extensible OOP design.
Project: Simple “Pet Shop” or “Digital Zoo” Application
Let’s outline how you could structure a simple interactive application to manage a collection of these pets.
Goal: Allow a user to add different types of pets and view all pets with their details and sounds.
Main Data Structure:
A list that will hold all our pet objects:
pet_shop_animals = []
Key Functions (Conceptual Outline):
-
add_new_pet(pet_list: list):- This function will manage adding a new pet.
- It should first ask the user what type of pet they want to add: "Enter pet type (dog, cat, or generic pet): ".
- Then, it gets common details:
name = input("Enter pet's name: "),age = int(input("Enter pet's age: ")). (Remembertry-exceptforint()conversion!) - If type is “dog”:
- Ask for dog-specific details:
breed = input("Enter dog's breed: "). - Ask for its specific sound:
dog_sound = input("What sound does this dog make (e.g., Woof)? "). - Create the
Dogobject:new_animal = Dog(name, age, dog_sound, breed).
- Ask for dog-specific details:
- Else if type is “cat”:
- Ask for cat-specific details:
fur_color = input("Enter cat's fur color: "). - Ask for its specific sound:
cat_sound = input("What sound does this cat make (e.g., Meow)? "). - Create the
Catobject:new_animal = Cat(name, age, cat_sound, fur_color).
- Ask for cat-specific details:
- Else (for “generic pet” or if type is unrecognized):
- Ask for its general sound:
pet_sound = input("What sound does this pet make? "). - Create a
Petobject:new_animal = Pet(name, age, pet_sound).
- Ask for its general sound:
- Finally, append the
new_animalobject to thepet_listpassed into the function. - Print a success message.
-
view_all_pets(pet_list: list):- This function is straightforward.
- Check if
pet_listis empty. If so, print “No pets in the shop yet!” - If not empty, loop through
pet_list(like our polymorphism example):# for pet_object in pet_list: # pet_object.show_details() # This will call the specific version for Dog, Cat, or Pet # pet_object.make_sound() # Same here # if isinstance(pet_object, Dog): # pet_object.fetch("virtual toy") # Maybe add a little extra for fun # elif isinstance(pet_object, Cat): # pet_object.chase_laser_pointer() # print("-" * 10) # Separator
Main Application Loop (Conceptual):
This would be similar to our To-Do List or Calculator app’s main loop.
# def run_pet_shop_app():
# pet_shop_animals = [] # Our main list of pet objects
# while True:
# print("\nPet Shop Menu:")
# print("1. Add a new pet")
# print("2. View all pets")
# print("3. Exit")
# choice = input("Enter choice: ")
# if choice == '1':
# add_new_pet(pet_shop_animals)
# elif choice == '2':
# view_all_pets(pet_shop_animals)
# elif choice == '3':
# print("Exiting Pet Shop. Goodbye!")
# break
# else:
# print("Invalid choice.")
Note on File I/O for Objects:
Saving a list of objects of different types (like Dog, Cat, Pet) to a JSON file and then loading them back correctly as instances of their original specific classes is more complex than saving a list of simple strings or basic dictionaries.
- When saving, you’d need to store an extra piece of information for each object indicating its class type (e.g.,
{"type": "Dog", "name": "Raja", ...}). - When loading, you’d read this “type” information and then use
if/elif/elseto create an instance of the correct class (Dog(...)orCat(...)) using the rest of the loaded data.
This is a more advanced topic, often called serialization/deserialization of custom objects, and might be covered in future, more advanced studies. For this lecture’s project, we’ll focus on managing the objects in memory. The homework challenge touches on this if you’re curious!
Recap of Concepts Learned
This lecture introduced powerful Object-Oriented Programming concepts:
- Inheritance: The mechanism where a new class (subclass/child) derives attributes and methods from an existing class (superclass/parent). This promotes an “Is-A” relationship (a
Dogis aPet). class Child(Parent):: The syntax for defining a subclass that inherits from a parent.super().__init__(...): How a child class constructor calls the parent class’s constructor to initialize inherited attributes. This is crucial for proper initialization.- Method Overriding: When a subclass provides its own specific implementation of a method that is already defined in its parent class. The subclass’s version is used for objects of the subclass.
- Adding New Attributes/Methods in Subclasses: Subclasses can have their own unique attributes and methods in addition to those inherited from the parent, making them more specialized.
- (Optional) Class Attributes: Attributes defined directly in the class body, shared by all instances (e.g.,
Pet.is_alive). - Working with Lists of Custom Objects: How to store objects of different (but related) classes in a single list.
- Basic Idea of Polymorphism (“Many Forms”): When you iterate through a list of objects from different classes in an inheritance hierarchy and call a common method (like
make_sound()), Python automatically executes the correct version of that method for each object’s actual class. isinstance(object, ClassName): A useful function to check if an object is an instance of a particular class (or one of its subclasses).
Inheritance is a cornerstone of OOP, allowing for elegant, reusable, and hierarchical code structures that can closely model real-world relationships.
Homework Challenges
Let’s practice these OOP concepts!
-
Expand the Pet Hierarchy:
- Create a new subclass of
Pet, for example,Bird. - Give
Birdits own__init__method that callssuper().__init__and also initializes bird-specific attributes likewing_span(e.g., in centimeters) andcan_fly(a booleanTrue/False). - Override the
make_soundmethod forBird(e.g., to “chirp”, “squawk”, etc., based on what you pass to itssoundparameter in__init__). - Add a new method specific to
Bird, likefly(self)which might print something like “[Bird’s Name] is flying with a wingspan of [wing_span] cm!” ifself.can_flyisTrue, or “[Bird’s Name] cannot fly.” if it’sFalse. - Modify the “Pet Shop” app’s
add_new_petfunction to include “bird” as an option and prompt for the bird-specific details. - Create some
Birdobjects and test their methods.
- Create a new subclass of
-
More Specific Methods for Subclasses:
- Add more unique methods to your
DogandCatclasses.- For
Dog: Maybe aguard_house(self)method. - For
Cat: Maybe asleep_in_sunbeam(self)method.
- For
- Update your
view_all_petsfunction in the Pet Shop app to check for these types and call these new methods if the pet is of the correct type.
- Add more unique methods to your
-
Extend the Pet Shop Application:
- Add a Search Function: Allow the user to search for pets by name. Display the details of any matching pets.
- Add a Remove Pet Function: Allow the user to remove a pet from the
pet_shop_animalslist (perhaps after searching for it or by selecting from a numbered list).
-
Advanced Challenge (Data Persistence for Objects):
- Research or think about how you would save the
pet_shop_animalslist (which contains objects of different subclasses likeDog,Cat,Bird) to a JSON file. - How would you store the type of each pet so that when you load the data, you can recreate the correct
Dog,Cat, orBirdobjects? - Hint: When saving, each pet’s dictionary in JSON could include an extra key like
"pet_type": "Dog". When loading, you’d read this key and useif/elif/elseto decide which class constructor (Dog(...),Cat(...)) to call. This is a common pattern for serializing/deserializing collections of diverse objects.
- Research or think about how you would save the
-
Alternative Project Idea: Vehicle Hierarchy:
- If pets aren’t your thing, try modeling a
Vehiclehierarchy. - Base Class:
Vehiclewith attributes likemake,model,year,speed, and methods likeaccelerate(),brake(),display_info(). - Subclasses:
Car(Vehicle): Add attributes likenum_doors,is_electric(boolean). Overridedisplay_info(). Add methodopen_trunk().Motorcycle(Vehicle): Add attributehas_sidecar(boolean). Overridedisplay_info(). Add methoddo_wheelie().Bicycle(Vehicle): Add attributenum_gears. Overridedisplay_info()(maybe it doesn’t have a speed in the same way). Add methodring_bell().
- Create instances and test their unique and inherited methods.
- If pets aren’t your thing, try modeling a
These challenges will help you get more comfortable with inheritance, method overriding, and designing systems with multiple interacting classes. This is where programming starts to get really creative and powerful!