Module 15 - More Object-Oriented Programming Header

Module 15 - More Object-Oriented Programming

Inheritance

Inheritance is a fundamental concept in Object-Oriented Programming (OOP) that allows a class (called the subclass or derived class) to inherit attributes and methods from another class (called the superclass or base class). It enables code reuse and promotes the "is-a" relationship between classes, allowing you to create hierarchies of related objects with shared characteristics.

How Inheritance Works:

When a subclass inherits from a superclass, it gains access to all the attributes and methods of the superclass. This means that the subclass can use these attributes and methods as if they were defined directly within the subclass itself. In Python, inheritance is specified by placing the name of the superclass in parentheses after the name of the subclass during class definition.

Syntax:

class Superclass:
# superclass attributes and methods

class Subclass(Superclass):
# subclass attributes and methods

Why Inheritance is Useful:

  • Code Reusability: Inheritance promotes code reusability by allowing you to define common attributes and methods in a superclass, which can be inherited by multiple subclasses. This avoids duplicating code and leads to more concise and maintainable programs.
  • Modularity: Inheritance encourages breaking down complex systems into smaller, manageable parts. Superclasses can represent generic features, while subclasses can specialize and extend those features for specific use cases.
  • Abstraction: Inheritance allows you to build a hierarchy of classes, where each level of the hierarchy represents a higher level of abstraction. In other words, you can make classes that share certain attributes and methods with higher level classes, but add their own attributes and methods as well. Often, higher-level classes define common behavior, while lower-level classes can handle the specific details.

When to Use Inheritance:

Inheritance should be used when there is a clear hierarchical relationship between classes and when subclasses share a significant amount of common behavior with the superclass. It is most beneficial for:

  • Common Attributes and Methods: Multiple classes have common attributes and methods that can be grouped in a higher-level superclass.
  • Specialization: Subclasses represent more specific versions of the superclass and add additional attributes or behaviors.
  • Code Organization: Inheritance improves code organization by grouping related classes into a logical hierarchy.

Example:

Let's consider an example of a Shape superclass and two subclasses, Rectangle and Circle, which will inherit from Shape. The Shape class defines a generic shape with a color, and both Rectangle and Circle are specific types of shapes with additional attributes and methods.

# Define a parent class 'Shape'
class Shape:
# Constructor method for initializing the color attribute
def __init__(self, color):
self.color = color

# Abstract method for calculating the area of the shape (no implementation)
def area(self):
pass

# Define a child class 'Rectangle' that inherits from the 'Shape' class
class Rectangle(Shape):
# Constructor method for initializing color, length, and width attributes
def __init__(self, color, length, width):
# Call the constructor of the parent class (Shape) to set the color attribute
super().__init__(color)
# Set contructor attributes for height and width
self.length = length
self.width = width

# Method for calculating the area of the rectangle
def area(self):
return self.length * self.width

# Define another child class 'Circle' that also inherits from the 'Shape' class
class Circle(Shape):
# Constructor method for initializing color and radius attributes
def __init__(self, color, radius):
# Call the constructor of the parent class (Shape) to set the color attribute
super().__init__(color)
# Set constructor attribute for radius.
self.radius = radius

# Method for calculating the area of the circle
def area(self):
return 3.14 * self.radius * self.radius

In the code above, we have three classes: Shape, Rectangle, and Circle. The Shape class is the parent class, and Rectangle and Circle are child classes that inherit from Shape. Each class has an area() method that calculates the area of the corresponding shape (rectangle or circle).

Line-by-line explanation:

  1. class Shape: - Defines the Shape class, which will serve as the parent class for other shapes.
  2. def __init__(self, color): - Defines the constructor method (__init__) of the Shape class. It takes a color parameter and initializes the color attribute of the object.
  3. self.color = color - Sets the color attribute of the object to the value passed as the color parameter.
  4. def area(self): - Defines an abstract method area() within the Shape class. This method has no implementation and will be overridden in the child classes.
  5. class Rectangle(Shape): - Defines the Rectangle class, which inherits from the Shape class. It will have all the attributes and methods of the Shape class.
  6. def __init__(self, color, length, width): - Defines the constructor method of the Rectangle class. It takes color, length, and width parameters and initializes the corresponding attributes of the object.
  7. super().__init__(color) - Calls the constructor of the parent class (Shape) using the super() function to set the color attribute for the Rectangle object.
  8. self.length = length - Sets the length attribute of the Rectangle object to the value passed as the length parameter.
  9. self.width = width - Sets the width attribute of the Rectangle object to the value passed as the width parameter.
  10. def area(self): - Defines the area() method within the Rectangle class. It calculates and returns the area of the rectangle by multiplying its length and width attributes.
  11. class Circle(Shape): - Defines the Circle class, which also inherits from the Shape class.
  12. def __init__(self, color, radius): - Defines the constructor method of the Circle class. It takes color and radius parameters and initializes the corresponding attributes of the object.
  13. super().__init__(color) - Calls the constructor of the parent class (Shape) to set the color attribute for the Circle object.
  14. self.radius = radius - Sets the radius attribute of the Circle object to the value passed as the radius parameter.
  15. def area(self): - Defines the area() method within the Circle class. It calculates and returns the area of the circle using the formula 3.14 * radius * radius.

Implementing the Classes Above

rectangle = Rectangle("red", 5, 3)
print(rectangle.color) # Output: "red"
print(rectangle.area()) # Output: 15

circle = Circle("blue", 4)
print(circle.color) # Output: "blue"
print(circle.area()) # Output: 50.24

In this way, inheritance allows us to create a hierarchy of classes that share common functionality while allowing each subclass to specialize and extend that functionality. It promotes code reusability, modularity, and abstraction, making it an essential concept in OOP design.
Inheritance is like a family tree where traits and characteristics are passed from parent classes to child classes. A child class inherits attributes and methods from its parent class and can also have additional unique features.



Polymorphism

Polymorphism is a powerful concept in Object-Oriented Programming (OOP) that allows objects of different classes to be treated as objects of a common superclass. It enables a single interface to represent various data types, promoting flexibility and code reuse. In Python, polymorphism is achieved through method overriding and duck typing.

How Polymorphism Works:

Polymorphism allows different classes to share a common interface or method name, even though they may have different implementations. When a method is called on an object, Python looks for that method in the object's class. If the method is not found in the class, Python searches the class hierarchy until it finds the method.

Method Overriding:

Method overriding is a form of polymorphism that occurs when a subclass provides a specific implementation for a method that is already defined in its superclass. The subclass overrides the behavior of the superclass method.

Example:

class Animal:
def make_sound(self):
return "Some generic sound."

class Dog(Animal):
def make_sound(self):
return "Woof! Woof!"

class Cat(Animal):
def make_sound(self):
return “Meow!”

In this example, the Animal class has a method make_sound(), which provides a generic sound. The Dog and Cat subclasses override this method with their specific sounds. When we call make_sound() on a Dog or Cat object, Python will use the overridden method from the corresponding subclass.

dog = Dog()
cat = Cat()

print(dog.make_sound()) # Output: "Woof! Woof!"
print(cat.make_sound()) # Output: “Meow!”

Duck Typing:

Python's dynamic typing system allows for a form of polymorphism called "duck typing." This means that if an object behaves like a certain type (quacks like a duck), it can be treated as that type without explicitly specifying its class inheritance.

Example:

class Car:
def drive(self):
print("Car is driving.")

class Bike:
def drive(self):
print("Bike is riding.")

def make_vehicle_drive(vehicle):
vehicle.drive()

car = Car()
bike = Bike()

make_vehicle_drive(car) # Output: "Car is driving."
make_vehicle_drive(bike) # Output: “Bike is riding.”

In this example, we have two classes, Car and Bike, each with a drive() method. The function make_vehicle_drive() can take either a Car object or a Bike object and call the drive() method on it without caring about the specific class type.

Why Polymorphism is Useful:

  • Flexibility: Polymorphism allows you to write more flexible and generic code that can handle different objects and data types without requiring explicit type checking.
  • Code Reusability: By using a common interface, you can reuse the same code to work with different classes, reducing code duplication.
  • Extensibility: You can easily add new classes that adhere to the common interface without modifying existing code, promoting scalability and maintainability.

When to Use Polymorphism:

Polymorphism is beneficial when you have multiple classes with similar behaviors or attributes but specific implementations. It is especially useful when you want to create generic functions or methods that can work with a variety of objects without needing to know their exact types.

Example:

Consider a Shape superclass with subclasses Rectangle and Circle. Each subclass has its own implementation of the area() method, demonstrating polymorphism.

class Shape:
def area(self):
pass

class Rectangle(Shape):
def __init__(self, length, width):
self.length = length
self.width = width

def area(self):
return self.length * self.width

class Circle(Shape):
def __init__(self, radius):
self.radius = radius

def area(self):
return 3.14 * self.radius * self.radius

In this example, the area() method is defined in the Shape superclass but overridden in the Rectangle and Circle subclasses. When calling area() on objects of Rectangle or Circle, Python uses the specific implementation provided by each subclass, resulting in the correct area calculation.

rectangle = Rectangle(5, 3)
circle = Circle(4)

print(rectangle.area()) # Output: 15
print(circle.area()) # Output: 50.24

Polymorphism allows us to treat different shapes (Rectangle and Circle) uniformly as objects of the common superclass (Shape) and call the same method on them, making the code more flexible and reusable.

Polymorphism allows different objects to be treated uniformly based on their common behavior. It provides a consistent interface to different classes, making it easier to work with various objects using the same method calls.



Example Code

Below is example code for a Vehicle Inventory System with Subclasses for Cars and Trucks, demonstrating Inheritance and Polymorphism:

# Define the parent class 'Vehicle'
class Vehicle:
# Constructor method for initializing common attributes
def __init__(self, make, model, year, last_service_date, odometer):
self.make = make
self.model = model
self.year = year
self.last_service_date = last_service_date
self.odometer = odometer

# Method to get the type of vehicle (to be overridden by subclasses)
def get_vehicle_type(self):
return "Generic Vehicle"

# Define the subclass 'Car' that inherits from 'Vehicle'
class Car(Vehicle):
# Constructor method for initializing car-specific attributes and calling the parent constructor
def __init__(self, make, model, year, last_service_date, odometer, num_doors):
super().__init__(make, model, year, last_service_date, odometer)
self.num_doors = num_doors

# Override the get_vehicle_type() method to return the specific type of vehicle
def get_vehicle_type(self):
return "Car"

# Define the subclass 'Truck' that also inherits from 'Vehicle'
class Truck(Vehicle):
# Constructor method for initializing truck-specific attributes and calling the parent constructor
def __init__(self, make, model, year, last_service_date, odometer, payload):
super().__init__(make, model, year, last_service_date, odometer)
self.payload = payload

# Override the get_vehicle_type() method to return the specific type of vehicle
def get_vehicle_type(self):
return "Truck"

Explanation of the Code Above:

  1. class Vehicle: - Defines the parent class Vehicle which represents common attributes and behavior for all types of vehicles.
  2. def __init__(self, make, model, year, last_service_date, odometer): - Defines the constructor method for the Vehicle class. It initializes common attributes such as make, model, year, last_service_date, and odometer.
  3. self.make = make, self.model = model, etc. - Sets the values of the common attributes for each object created from the Vehicle class.
  4. def get_vehicle_type(self): - Defines a method get_vehicle_type() within the Vehicle class. This method is intended to be overridden by subclasses and returns a generic type of vehicle.
  5. class Car(Vehicle): - Defines the subclass Car that inherits from the Vehicle class. It represents cars and adds car-specific attributes.
  6. def __init__(self, make, model, year, last_service_date, odometer, num_doors): - Defines the constructor method for the Car class. It calls the constructor of the parent class (Vehicle) using super() to initialize common attributes and sets the car-specific attribute num_doors.
  7. super().__init__(make, model, year, last_service_date, odometer) - Calls the constructor of the parent class (Vehicle) to set common attributes for the Car object.
  8. def get_vehicle_type(self): - Overrides the get_vehicle_type() method in the Car class. It returns the specific type of vehicle, which is "Car" in this case.
  9. class Truck(Vehicle): - Defines the subclass Truck that also inherits from the Vehicle class. It represents trucks and adds truck-specific attributes.
  10. def __init__(self, make, model, year, last_service_date, odometer, payload): - Defines the constructor method for the Truck class. It calls the constructor of the parent class (Vehicle) using super() to initialize common attributes and sets the truck-specific attribute payload.
  11. super().__init__(make, model, year, last_service_date, odometer) - Calls the constructor of the parent class (Vehicle) to set common attributes for the Truck object.
  12. def get_vehicle_type(self): - Overrides the get_vehicle_type() method in the Truck class. It returns the specific type of vehicle, which is "Truck" in this case.

Example of Polymorphism:

Now, let's demonstrate polymorphism by creating a list of vehicles (cars and trucks) and calling the get_vehicle_type() method for each vehicle. Despite having different subclasses, the method behaves uniformly for all objects in the list.

# Create a list of vehicles (cars and trucks)
vehicles = [
Car("Toyota", "Corolla", 2021, "2023-07-15", 50000, 4),
Truck("Ford", "F-150", 2020, "2023-07-10", 80000, 2000),
Car("Honda", "Civic", 2019, "2023-07-20", 40000, 2),
Truck("Chevrolet", "Silverado", 2018, "2023-07-05", 90000, 3000)
]

# Call the get_vehicle_type() method for each vehicle
for vehicle in vehicles:
print(f"{vehicle.make} {vehicle.model} is a {vehicle.get_vehicle_type()}.")

Output:

Toyota Corolla is a Car.
Ford F-150 is a Truck.
Honda Civic is a Car.
Chevrolet Silverado is a Truck.

As shown in the output, the get_vehicle_type() method behaves uniformly for both cars and trucks, demonstrating polymorphism. This allows us to work with objects of different classes using a common interface, making the code more flexible and reusable.

Videos for Module 15 - More Object-Oriented Programming

15-1: Introducing Module 15 (1:08)

15-2: Object-Oriented Programming - Inheritance (3:27)

15-3: Object-Oriented Programming - Inheritance Code Example (5:58)

15-4: Object-Oriented Programming - Polymorphism (3:45)

15-5: Object-Oriented Programming - Polymorphism Code Example (3:46)

15-6: Extending Existing Classes - Turtle Example (4:57)

15-7: S15 Explanation (5:24)

15-8: A15 Explanation (4:57)

Key Terms for Module 15 - More Object-Oriented Programming

No terms have been published for this module.

Quiz Yourself - Module 15 - More Object-Oriented Programming

Test your knowledge of this module by choosing options below. You can keep trying until you get the right answer.

Skip to the Next Question 

Activities for this Module

S15 - E-Pet Evolution

Note: Sandbox assignments are designed to be formative activities that are somewhat open-ended. To get the most value, spend some time playing around as you code.

Ok - For this last sandbox challenge we will be building on what we did in the last module with our virtual pets. Start by downloading the oop_virtual_pet2.py file and opening it in PyCharm.

Download the oop_virtual_pet2.py File

This file is a bit different. You will see that the clPet class is still there, but in addition to it there are now two more classes: clPetFish and clPetDog.

You might also notice that these “child” classes have a reference to “clPet” in the parenthesis on their class line, which means that they will inherit the properties and methods of the clPet class. They will have a name, hunger and boredom, and will also have the ability to feedPet, playPet and takeTurn. Inheritance is one of the powerful ideas and aspects of OOP.  Used properly, it can create some pretty powerful code.

If you look at the __init__ method of these new classes, you will see two things:

First, you will see a line that starts with super( ).__init__(self, strName) — This line triggers the __init__ method of clPet when a clPetFish or clPetDog is created, creating the properties and also passing the strName property to the super (or parent) class.

Second, you will see that in these subclasses, we also create a property called type, which tells us what type of animal it is.

There is one more thing that is interesting about these subclasses:

They have a playPet method defined. Since a playPet method is inherited from clPet, the new playPet overrides the original playPet method, printing out an animal-specific line when we play with them. When we do this in OOP, it is called polymorphism.

Your challenge for this sandbox is to take this code, and add at least one more type of animal.  You will need to create a subclass for your new animal type(s), and also edit the menu structure to allow the user to adopt this new animal.  

A15 - Object-Oriented ATM

The Challenge

This week, you will use OOP and inheritance to make an even better ATM program.  Follow along with the video for this week to get you most of the way there.

Here's a Starter File with everything except the savingsAccount and CheckingAccount classes completed:

import time
class bankAccount:
def __init__(self, strName, intBalance):
self.name = strName
self.balance = intBalance
def withdraw(self,flAmount):
if self.balance >= flAmount:
self.balance -= flAmount
print("You have withdrawn ${0:.2f} from {1}".format(flAmount,self.name))
else:
print("You are broke.")
sleepClear(1)
def deposit(self,flAmount):
self.balance += flAmount
print("You have deposited ${0:.2f} into {1}".format(flAmount, self.name))
sleepClear(1)
def ckBalance(self):
print("You have ${0:.2f} in {1}.".format(self.balance, self.name))
sleepClear(1)

class savingsAcct(bankAccount):
def __init__(self,strName, intBalance):
super().__init__(strName, intBalance)
print("You have created savings account: {0}".format(self.name))

class checkingAcct(bankAccount):
def __init__(self,strName, intBalance):


def writeCheck(self,flAmount):




def sleepClear(flTime):
time.sleep(flTime)
print("\n"*30)

def mainMenu():
print("$"*80)
print("$$"," Moseley Bank ".center(74),"$$")
print("$"*80)
print("What do you want to do?")
print("1. Deposit Money")
print("2. Withdraw")
print("3. Write a Check")
print("4. Check Balance")
print("5. Add an Account")
strTmp = input("Enter Choice Here:")
return strTmp

def showAccounts(lisAccounts):
intIndex = 1
for account in lisAccounts:
str1 = str(intIndex).ljust(5)
str2 = str(account.name).ljust(30,".")
str3 = "${0:.2f}".format(account.balance).rjust(20,".")
print(str1+str2+str3)
intIndex += 1
pressEnter()

def pressEnter():
input("\nPress Enter/Return to continue...")
print("\n"*20)

def createAccount(lisAccounts):
strAccountType = input("Do you want a (1) Checking or (2) Savings account? ")
strAcctName = input("What should the account be called? ")
if strAccountType == "1":
lisAccounts.append(checkingAcct(strAcctName, 50))
elif strAccountType == "2":
lisAccounts.append(savingsAcct(strAcctName, 50))
else:
print("That is not a valid choice.")
showAccounts(lisAccounts)
return lisAccounts

def makeDeposit(lisAccounts):
showAccounts(lisAccounts)
strAccount = input("Which Account?")
strAmount = input("How Much?")
if strAccount.isnumeric() and int(strAccount) <= len(lisAccounts):
intIndex = int(strAccount) - 1
lisAccounts[intIndex].deposit(float(strAmount))



lisAccounts = []
lisAccounts.append(savingsAcct("Main Savings",30))
lisAccounts.append(checkingAcct("Main Checking", 305))

while True:
strMenu = mainMenu()
if strMenu == "1":
print("Make a deposit")
makeDeposit(lisAccounts)
elif strMenu == "2":
print("Make a withdrawal")
elif strMenu == "3":
print("Write a Check")
showAccounts(lisAccounts)
try:
strChoice = input("Which account do you want? ")
flAmount = float(input("How much is the check for? "))
intChoice = int(strChoice) - 1
lisAccounts[intChoice].writeCheck(flAmount)
except:
print("Something went wrong. Try again.")

elif strMenu == "4":
print("Check Balances")
showAccounts(lisAccounts)
elif strMenu == "5":
print("Add an Account")
lisAccounts = createAccount(lisAccounts)
else:
print("invalid entry")
pressEnter()
 

Constraints

  1. Must use OOP and Inheritance.
  2. Must be clean, with error checking and UI.

Submission Type

Please submit the complete program as a .py file.