Menu iconMenu iconPython Programming Unlocked for Beginners
Python Programming Unlocked for Beginners

Chapter 8: Object-Oriented Programming

8.3: Inheritance

Inheritance is one of the most powerful features in object-oriented programming, and it can greatly enhance the efficiency of your code. By allowing one class to inherit the attributes and methods of another class, you can create new classes that are built on top of existing ones, which can save a lot of time and effort. 

For example, let's say you have a class that represents a car, with attributes like the make, model, and year, and methods like start_engine and accelerate. Now, let's say you want to create a new class for a specific type of car, like a sports car. Instead of starting from scratch and defining all of the attributes and methods for the sports car, you can simply create a new class that inherits from the car class, and then add or modify the attributes and methods as necessary. This way, you can reuse much of the code that you already wrote for the car class, and only focus on the changes that are specific to the sports car. 

In Python, inheritance is implemented by defining a new class that takes the parent (base) class as an argument. The new class is called the child (derived) class, and the class it inherits from is called the parent (base) class. By using inheritance, you can create complex class hierarchies that can help you organize your code and make it more modular and reusable. Overall, inheritance is an essential concept in object-oriented programming, and mastering it can greatly improve your coding skills.

Let's understand inheritance through an example.

Suppose we have a base class called Animal that represents a generic animal, with attributes like nameage, and a method called speak():

class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def speak(self):
        print(f"{self.name} makes a noise") 

Now, we want to create a class Dog that represents a dog, which is a specific type of animal. Instead of redefining all the attributes and methods of the Animal class, we can inherit them using inheritance:

class Dog(Animal):
    def __init__(self, name, age, breed):
        super().__init__(name, age)
        self.breed = breed

    def speak(self):
        print(f"{self.name} barks")

In this example, we define the Dog class and inherit from the Animal class. We use the super() function to call the __init__ method of the parent class to initialize the name and age attributes. We also add a new attribute, breed, specific to the Dog class.

We also override the speak() method of the Animal class to better fit the behavior of a dog. This is called method overriding and is a common practice when using inheritance.

Now, when we create a Dog object, it will have access to both the attributes and methods of the Animal class, as well as any new attributes or methods we define in the Dog class:

dog1 = Dog("Max", 3, "Labrador")
print(dog1.name)  # Output: Max
print(dog1.age)  # Output: 3
print(dog1.breed)  # Output: Labrador
dog1.speak()  # Output: Max barks

Inheritance is a fundamental concept in object-oriented programming that allows for the creation of more specialized classes. It enables code reuse from more general classes while providing the ability to customize or extend their behavior as needed. This makes inheritance a powerful tool for developers to create efficient and effective software solutions.

By using inheritance, developers can create a hierarchy of classes that share common characteristics, allowing for the implementation of complex systems. Additionally, inheritance helps reduce the complexity of code, making it easier to maintain and update over time. Therefore, it is essential to have a solid understanding of inheritance when developing software applications, as it can greatly improve the overall quality of the code and make it more scalable.

Exercise 8.3.1: Simple Inheritance

Implement a class hierarchy for different types of vehicles, using inheritance.

Instructions:

  1. Create a base class called Vehicle with attributes makemodel, and year.
  2. Define a method vehicle_info() that prints the make, model, and year of the vehicle.
  3. Create a class Car that inherits from Vehicle and has an additional attribute doors.
  4. Create a class Motorcycle that inherits from Vehicle and has an additional attribute type.
  5. Create instances of Car and Motorcycle, and call the vehicle_info() method for each.

Solution:

class Vehicle:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year

    def vehicle_info(self):
        print(f"{self.year} {self.make} {self.model}")

class Car(Vehicle):
    def __init__(self, make, model, year, doors):
        super().__init__(make, model, year)
        self.doors = doors

class Motorcycle(Vehicle):
    def __init__(self, make, model, year, bike_type):
        super().__init__(make, model, year)
        self.type = bike_type

car1 = Car("Toyota", "Camry", 2020, 4)
car1.vehicle_info()  # Output: 2020 Toyota Camry

motorcycle1 = Motorcycle("Yamaha", "R1", 2019, "Sport")
motorcycle1.vehicle_info()  # Output: 2019 Yamaha R1

Exercise 8.3.2: Inheritance and Method Overriding

Create classes representing different types of bank accounts, using inheritance and method overriding.

Instructions:

  1. Create a base class BankAccount with attributes balance and a method deposit().
  2. Define a method withdraw() that checks if the withdrawal amount is less than or equal to the balance, and updates the balance accordingly.
  3. Create a class SavingsAccount that inherits from BankAccount and has an additional attribute interest_rate.
  4. Override the withdraw() method in SavingsAccount to include a withdrawal fee of 1% of the withdrawal amount.
  5. Create instances of BankAccount and SavingsAccount, and test the deposit() and withdraw() methods.

Solution:

class BankAccount:
    def __init__(self):
        self.balance = 0

    def deposit(self, amount):
        self.balance += amount

    def withdraw(self, amount):
        if amount <= self.balance:
            self.balance -= amount
        else:
            print("Insufficient funds")

class SavingsAccount(BankAccount):
    def __init__(self, interest_rate):
        super().__init__()
        self.interest_rate = interest_rate

    def withdraw(self, amount):
        fee = amount * 0.01
        if amount + fee <= self.balance:
            self.balance -= (amount + fee)
        else:
            print("Insufficient funds")

account1 = BankAccount()
account1.deposit(100)
account1.withdraw(50)
print(account1.balance)  # Output: 50

savings1 = SavingsAccount(0.02)
savings1.deposit(100)
savings1.withdraw(50)
print(savings1.balance)  # Output: 49.5

Exercise 8.3.3: Multiple Inheritance

 Implement a class hierarchy using multiple inheritance.

Instructions:

  1. Create a class Person with attributes first_name and last_name.
  2. Create a class Employee that inherits from Person and has an additional attribute employee_id.
  3. Create a class Student that inherits from Person and has an additional attribute student_id.
  4. Create a class TeachingAssistant that inherits from both Employee and Student.
  5. Define a method get_info() for each class that returns a string with the person's information.
  6. Create an instance of TeachingAssistant and call the get_info() method.

Solution:

class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    def get_info(self):
        return f"{self.first_name} {self.last_name}"

class Employee(Person):
    def __init__(self, first_name, last_name, employee_id):
        super().__init__(first_name, last_name)
        self.employee_id = employee_id

    def get_info(self):
        return f"{super().get_info()}, Employee ID: {self.employee_id}"

class Student(Person):
    def __init__(self, first_name, last_name, student_id):
        super().__init__(first_name, last_name)
        self.student_id = student_id

    def get_info(self):
        return f"{super().get_info()}, Student ID: {self.student_id}"

class TeachingAssistant(Employee, Student):
    def __init__(self, first_name, last_name, employee_id, student_id):
        Employee.__init__(self, first_name, last_name, employee_id)
        Student.__init__(self, first_name, last_name, student_id)

    def get_info(self):
        return f"{super().get_info()}, Employee ID: {self.employee_id}, Student ID: {self.student_id}"

ta1 = TeachingAssistant("John", "Doe", 1001, 2001)
print(ta1.get_info())  # Output: John Doe, Employee ID: 1001, Student ID: 2001

8.3: Inheritance

Inheritance is one of the most powerful features in object-oriented programming, and it can greatly enhance the efficiency of your code. By allowing one class to inherit the attributes and methods of another class, you can create new classes that are built on top of existing ones, which can save a lot of time and effort. 

For example, let's say you have a class that represents a car, with attributes like the make, model, and year, and methods like start_engine and accelerate. Now, let's say you want to create a new class for a specific type of car, like a sports car. Instead of starting from scratch and defining all of the attributes and methods for the sports car, you can simply create a new class that inherits from the car class, and then add or modify the attributes and methods as necessary. This way, you can reuse much of the code that you already wrote for the car class, and only focus on the changes that are specific to the sports car. 

In Python, inheritance is implemented by defining a new class that takes the parent (base) class as an argument. The new class is called the child (derived) class, and the class it inherits from is called the parent (base) class. By using inheritance, you can create complex class hierarchies that can help you organize your code and make it more modular and reusable. Overall, inheritance is an essential concept in object-oriented programming, and mastering it can greatly improve your coding skills.

Let's understand inheritance through an example.

Suppose we have a base class called Animal that represents a generic animal, with attributes like nameage, and a method called speak():

class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def speak(self):
        print(f"{self.name} makes a noise") 

Now, we want to create a class Dog that represents a dog, which is a specific type of animal. Instead of redefining all the attributes and methods of the Animal class, we can inherit them using inheritance:

class Dog(Animal):
    def __init__(self, name, age, breed):
        super().__init__(name, age)
        self.breed = breed

    def speak(self):
        print(f"{self.name} barks")

In this example, we define the Dog class and inherit from the Animal class. We use the super() function to call the __init__ method of the parent class to initialize the name and age attributes. We also add a new attribute, breed, specific to the Dog class.

We also override the speak() method of the Animal class to better fit the behavior of a dog. This is called method overriding and is a common practice when using inheritance.

Now, when we create a Dog object, it will have access to both the attributes and methods of the Animal class, as well as any new attributes or methods we define in the Dog class:

dog1 = Dog("Max", 3, "Labrador")
print(dog1.name)  # Output: Max
print(dog1.age)  # Output: 3
print(dog1.breed)  # Output: Labrador
dog1.speak()  # Output: Max barks

Inheritance is a fundamental concept in object-oriented programming that allows for the creation of more specialized classes. It enables code reuse from more general classes while providing the ability to customize or extend their behavior as needed. This makes inheritance a powerful tool for developers to create efficient and effective software solutions.

By using inheritance, developers can create a hierarchy of classes that share common characteristics, allowing for the implementation of complex systems. Additionally, inheritance helps reduce the complexity of code, making it easier to maintain and update over time. Therefore, it is essential to have a solid understanding of inheritance when developing software applications, as it can greatly improve the overall quality of the code and make it more scalable.

Exercise 8.3.1: Simple Inheritance

Implement a class hierarchy for different types of vehicles, using inheritance.

Instructions:

  1. Create a base class called Vehicle with attributes makemodel, and year.
  2. Define a method vehicle_info() that prints the make, model, and year of the vehicle.
  3. Create a class Car that inherits from Vehicle and has an additional attribute doors.
  4. Create a class Motorcycle that inherits from Vehicle and has an additional attribute type.
  5. Create instances of Car and Motorcycle, and call the vehicle_info() method for each.

Solution:

class Vehicle:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year

    def vehicle_info(self):
        print(f"{self.year} {self.make} {self.model}")

class Car(Vehicle):
    def __init__(self, make, model, year, doors):
        super().__init__(make, model, year)
        self.doors = doors

class Motorcycle(Vehicle):
    def __init__(self, make, model, year, bike_type):
        super().__init__(make, model, year)
        self.type = bike_type

car1 = Car("Toyota", "Camry", 2020, 4)
car1.vehicle_info()  # Output: 2020 Toyota Camry

motorcycle1 = Motorcycle("Yamaha", "R1", 2019, "Sport")
motorcycle1.vehicle_info()  # Output: 2019 Yamaha R1

Exercise 8.3.2: Inheritance and Method Overriding

Create classes representing different types of bank accounts, using inheritance and method overriding.

Instructions:

  1. Create a base class BankAccount with attributes balance and a method deposit().
  2. Define a method withdraw() that checks if the withdrawal amount is less than or equal to the balance, and updates the balance accordingly.
  3. Create a class SavingsAccount that inherits from BankAccount and has an additional attribute interest_rate.
  4. Override the withdraw() method in SavingsAccount to include a withdrawal fee of 1% of the withdrawal amount.
  5. Create instances of BankAccount and SavingsAccount, and test the deposit() and withdraw() methods.

Solution:

class BankAccount:
    def __init__(self):
        self.balance = 0

    def deposit(self, amount):
        self.balance += amount

    def withdraw(self, amount):
        if amount <= self.balance:
            self.balance -= amount
        else:
            print("Insufficient funds")

class SavingsAccount(BankAccount):
    def __init__(self, interest_rate):
        super().__init__()
        self.interest_rate = interest_rate

    def withdraw(self, amount):
        fee = amount * 0.01
        if amount + fee <= self.balance:
            self.balance -= (amount + fee)
        else:
            print("Insufficient funds")

account1 = BankAccount()
account1.deposit(100)
account1.withdraw(50)
print(account1.balance)  # Output: 50

savings1 = SavingsAccount(0.02)
savings1.deposit(100)
savings1.withdraw(50)
print(savings1.balance)  # Output: 49.5

Exercise 8.3.3: Multiple Inheritance

 Implement a class hierarchy using multiple inheritance.

Instructions:

  1. Create a class Person with attributes first_name and last_name.
  2. Create a class Employee that inherits from Person and has an additional attribute employee_id.
  3. Create a class Student that inherits from Person and has an additional attribute student_id.
  4. Create a class TeachingAssistant that inherits from both Employee and Student.
  5. Define a method get_info() for each class that returns a string with the person's information.
  6. Create an instance of TeachingAssistant and call the get_info() method.

Solution:

class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    def get_info(self):
        return f"{self.first_name} {self.last_name}"

class Employee(Person):
    def __init__(self, first_name, last_name, employee_id):
        super().__init__(first_name, last_name)
        self.employee_id = employee_id

    def get_info(self):
        return f"{super().get_info()}, Employee ID: {self.employee_id}"

class Student(Person):
    def __init__(self, first_name, last_name, student_id):
        super().__init__(first_name, last_name)
        self.student_id = student_id

    def get_info(self):
        return f"{super().get_info()}, Student ID: {self.student_id}"

class TeachingAssistant(Employee, Student):
    def __init__(self, first_name, last_name, employee_id, student_id):
        Employee.__init__(self, first_name, last_name, employee_id)
        Student.__init__(self, first_name, last_name, student_id)

    def get_info(self):
        return f"{super().get_info()}, Employee ID: {self.employee_id}, Student ID: {self.student_id}"

ta1 = TeachingAssistant("John", "Doe", 1001, 2001)
print(ta1.get_info())  # Output: John Doe, Employee ID: 1001, Student ID: 2001

8.3: Inheritance

Inheritance is one of the most powerful features in object-oriented programming, and it can greatly enhance the efficiency of your code. By allowing one class to inherit the attributes and methods of another class, you can create new classes that are built on top of existing ones, which can save a lot of time and effort. 

For example, let's say you have a class that represents a car, with attributes like the make, model, and year, and methods like start_engine and accelerate. Now, let's say you want to create a new class for a specific type of car, like a sports car. Instead of starting from scratch and defining all of the attributes and methods for the sports car, you can simply create a new class that inherits from the car class, and then add or modify the attributes and methods as necessary. This way, you can reuse much of the code that you already wrote for the car class, and only focus on the changes that are specific to the sports car. 

In Python, inheritance is implemented by defining a new class that takes the parent (base) class as an argument. The new class is called the child (derived) class, and the class it inherits from is called the parent (base) class. By using inheritance, you can create complex class hierarchies that can help you organize your code and make it more modular and reusable. Overall, inheritance is an essential concept in object-oriented programming, and mastering it can greatly improve your coding skills.

Let's understand inheritance through an example.

Suppose we have a base class called Animal that represents a generic animal, with attributes like nameage, and a method called speak():

class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def speak(self):
        print(f"{self.name} makes a noise") 

Now, we want to create a class Dog that represents a dog, which is a specific type of animal. Instead of redefining all the attributes and methods of the Animal class, we can inherit them using inheritance:

class Dog(Animal):
    def __init__(self, name, age, breed):
        super().__init__(name, age)
        self.breed = breed

    def speak(self):
        print(f"{self.name} barks")

In this example, we define the Dog class and inherit from the Animal class. We use the super() function to call the __init__ method of the parent class to initialize the name and age attributes. We also add a new attribute, breed, specific to the Dog class.

We also override the speak() method of the Animal class to better fit the behavior of a dog. This is called method overriding and is a common practice when using inheritance.

Now, when we create a Dog object, it will have access to both the attributes and methods of the Animal class, as well as any new attributes or methods we define in the Dog class:

dog1 = Dog("Max", 3, "Labrador")
print(dog1.name)  # Output: Max
print(dog1.age)  # Output: 3
print(dog1.breed)  # Output: Labrador
dog1.speak()  # Output: Max barks

Inheritance is a fundamental concept in object-oriented programming that allows for the creation of more specialized classes. It enables code reuse from more general classes while providing the ability to customize or extend their behavior as needed. This makes inheritance a powerful tool for developers to create efficient and effective software solutions.

By using inheritance, developers can create a hierarchy of classes that share common characteristics, allowing for the implementation of complex systems. Additionally, inheritance helps reduce the complexity of code, making it easier to maintain and update over time. Therefore, it is essential to have a solid understanding of inheritance when developing software applications, as it can greatly improve the overall quality of the code and make it more scalable.

Exercise 8.3.1: Simple Inheritance

Implement a class hierarchy for different types of vehicles, using inheritance.

Instructions:

  1. Create a base class called Vehicle with attributes makemodel, and year.
  2. Define a method vehicle_info() that prints the make, model, and year of the vehicle.
  3. Create a class Car that inherits from Vehicle and has an additional attribute doors.
  4. Create a class Motorcycle that inherits from Vehicle and has an additional attribute type.
  5. Create instances of Car and Motorcycle, and call the vehicle_info() method for each.

Solution:

class Vehicle:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year

    def vehicle_info(self):
        print(f"{self.year} {self.make} {self.model}")

class Car(Vehicle):
    def __init__(self, make, model, year, doors):
        super().__init__(make, model, year)
        self.doors = doors

class Motorcycle(Vehicle):
    def __init__(self, make, model, year, bike_type):
        super().__init__(make, model, year)
        self.type = bike_type

car1 = Car("Toyota", "Camry", 2020, 4)
car1.vehicle_info()  # Output: 2020 Toyota Camry

motorcycle1 = Motorcycle("Yamaha", "R1", 2019, "Sport")
motorcycle1.vehicle_info()  # Output: 2019 Yamaha R1

Exercise 8.3.2: Inheritance and Method Overriding

Create classes representing different types of bank accounts, using inheritance and method overriding.

Instructions:

  1. Create a base class BankAccount with attributes balance and a method deposit().
  2. Define a method withdraw() that checks if the withdrawal amount is less than or equal to the balance, and updates the balance accordingly.
  3. Create a class SavingsAccount that inherits from BankAccount and has an additional attribute interest_rate.
  4. Override the withdraw() method in SavingsAccount to include a withdrawal fee of 1% of the withdrawal amount.
  5. Create instances of BankAccount and SavingsAccount, and test the deposit() and withdraw() methods.

Solution:

class BankAccount:
    def __init__(self):
        self.balance = 0

    def deposit(self, amount):
        self.balance += amount

    def withdraw(self, amount):
        if amount <= self.balance:
            self.balance -= amount
        else:
            print("Insufficient funds")

class SavingsAccount(BankAccount):
    def __init__(self, interest_rate):
        super().__init__()
        self.interest_rate = interest_rate

    def withdraw(self, amount):
        fee = amount * 0.01
        if amount + fee <= self.balance:
            self.balance -= (amount + fee)
        else:
            print("Insufficient funds")

account1 = BankAccount()
account1.deposit(100)
account1.withdraw(50)
print(account1.balance)  # Output: 50

savings1 = SavingsAccount(0.02)
savings1.deposit(100)
savings1.withdraw(50)
print(savings1.balance)  # Output: 49.5

Exercise 8.3.3: Multiple Inheritance

 Implement a class hierarchy using multiple inheritance.

Instructions:

  1. Create a class Person with attributes first_name and last_name.
  2. Create a class Employee that inherits from Person and has an additional attribute employee_id.
  3. Create a class Student that inherits from Person and has an additional attribute student_id.
  4. Create a class TeachingAssistant that inherits from both Employee and Student.
  5. Define a method get_info() for each class that returns a string with the person's information.
  6. Create an instance of TeachingAssistant and call the get_info() method.

Solution:

class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    def get_info(self):
        return f"{self.first_name} {self.last_name}"

class Employee(Person):
    def __init__(self, first_name, last_name, employee_id):
        super().__init__(first_name, last_name)
        self.employee_id = employee_id

    def get_info(self):
        return f"{super().get_info()}, Employee ID: {self.employee_id}"

class Student(Person):
    def __init__(self, first_name, last_name, student_id):
        super().__init__(first_name, last_name)
        self.student_id = student_id

    def get_info(self):
        return f"{super().get_info()}, Student ID: {self.student_id}"

class TeachingAssistant(Employee, Student):
    def __init__(self, first_name, last_name, employee_id, student_id):
        Employee.__init__(self, first_name, last_name, employee_id)
        Student.__init__(self, first_name, last_name, student_id)

    def get_info(self):
        return f"{super().get_info()}, Employee ID: {self.employee_id}, Student ID: {self.student_id}"

ta1 = TeachingAssistant("John", "Doe", 1001, 2001)
print(ta1.get_info())  # Output: John Doe, Employee ID: 1001, Student ID: 2001

8.3: Inheritance

Inheritance is one of the most powerful features in object-oriented programming, and it can greatly enhance the efficiency of your code. By allowing one class to inherit the attributes and methods of another class, you can create new classes that are built on top of existing ones, which can save a lot of time and effort. 

For example, let's say you have a class that represents a car, with attributes like the make, model, and year, and methods like start_engine and accelerate. Now, let's say you want to create a new class for a specific type of car, like a sports car. Instead of starting from scratch and defining all of the attributes and methods for the sports car, you can simply create a new class that inherits from the car class, and then add or modify the attributes and methods as necessary. This way, you can reuse much of the code that you already wrote for the car class, and only focus on the changes that are specific to the sports car. 

In Python, inheritance is implemented by defining a new class that takes the parent (base) class as an argument. The new class is called the child (derived) class, and the class it inherits from is called the parent (base) class. By using inheritance, you can create complex class hierarchies that can help you organize your code and make it more modular and reusable. Overall, inheritance is an essential concept in object-oriented programming, and mastering it can greatly improve your coding skills.

Let's understand inheritance through an example.

Suppose we have a base class called Animal that represents a generic animal, with attributes like nameage, and a method called speak():

class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def speak(self):
        print(f"{self.name} makes a noise") 

Now, we want to create a class Dog that represents a dog, which is a specific type of animal. Instead of redefining all the attributes and methods of the Animal class, we can inherit them using inheritance:

class Dog(Animal):
    def __init__(self, name, age, breed):
        super().__init__(name, age)
        self.breed = breed

    def speak(self):
        print(f"{self.name} barks")

In this example, we define the Dog class and inherit from the Animal class. We use the super() function to call the __init__ method of the parent class to initialize the name and age attributes. We also add a new attribute, breed, specific to the Dog class.

We also override the speak() method of the Animal class to better fit the behavior of a dog. This is called method overriding and is a common practice when using inheritance.

Now, when we create a Dog object, it will have access to both the attributes and methods of the Animal class, as well as any new attributes or methods we define in the Dog class:

dog1 = Dog("Max", 3, "Labrador")
print(dog1.name)  # Output: Max
print(dog1.age)  # Output: 3
print(dog1.breed)  # Output: Labrador
dog1.speak()  # Output: Max barks

Inheritance is a fundamental concept in object-oriented programming that allows for the creation of more specialized classes. It enables code reuse from more general classes while providing the ability to customize or extend their behavior as needed. This makes inheritance a powerful tool for developers to create efficient and effective software solutions.

By using inheritance, developers can create a hierarchy of classes that share common characteristics, allowing for the implementation of complex systems. Additionally, inheritance helps reduce the complexity of code, making it easier to maintain and update over time. Therefore, it is essential to have a solid understanding of inheritance when developing software applications, as it can greatly improve the overall quality of the code and make it more scalable.

Exercise 8.3.1: Simple Inheritance

Implement a class hierarchy for different types of vehicles, using inheritance.

Instructions:

  1. Create a base class called Vehicle with attributes makemodel, and year.
  2. Define a method vehicle_info() that prints the make, model, and year of the vehicle.
  3. Create a class Car that inherits from Vehicle and has an additional attribute doors.
  4. Create a class Motorcycle that inherits from Vehicle and has an additional attribute type.
  5. Create instances of Car and Motorcycle, and call the vehicle_info() method for each.

Solution:

class Vehicle:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year

    def vehicle_info(self):
        print(f"{self.year} {self.make} {self.model}")

class Car(Vehicle):
    def __init__(self, make, model, year, doors):
        super().__init__(make, model, year)
        self.doors = doors

class Motorcycle(Vehicle):
    def __init__(self, make, model, year, bike_type):
        super().__init__(make, model, year)
        self.type = bike_type

car1 = Car("Toyota", "Camry", 2020, 4)
car1.vehicle_info()  # Output: 2020 Toyota Camry

motorcycle1 = Motorcycle("Yamaha", "R1", 2019, "Sport")
motorcycle1.vehicle_info()  # Output: 2019 Yamaha R1

Exercise 8.3.2: Inheritance and Method Overriding

Create classes representing different types of bank accounts, using inheritance and method overriding.

Instructions:

  1. Create a base class BankAccount with attributes balance and a method deposit().
  2. Define a method withdraw() that checks if the withdrawal amount is less than or equal to the balance, and updates the balance accordingly.
  3. Create a class SavingsAccount that inherits from BankAccount and has an additional attribute interest_rate.
  4. Override the withdraw() method in SavingsAccount to include a withdrawal fee of 1% of the withdrawal amount.
  5. Create instances of BankAccount and SavingsAccount, and test the deposit() and withdraw() methods.

Solution:

class BankAccount:
    def __init__(self):
        self.balance = 0

    def deposit(self, amount):
        self.balance += amount

    def withdraw(self, amount):
        if amount <= self.balance:
            self.balance -= amount
        else:
            print("Insufficient funds")

class SavingsAccount(BankAccount):
    def __init__(self, interest_rate):
        super().__init__()
        self.interest_rate = interest_rate

    def withdraw(self, amount):
        fee = amount * 0.01
        if amount + fee <= self.balance:
            self.balance -= (amount + fee)
        else:
            print("Insufficient funds")

account1 = BankAccount()
account1.deposit(100)
account1.withdraw(50)
print(account1.balance)  # Output: 50

savings1 = SavingsAccount(0.02)
savings1.deposit(100)
savings1.withdraw(50)
print(savings1.balance)  # Output: 49.5

Exercise 8.3.3: Multiple Inheritance

 Implement a class hierarchy using multiple inheritance.

Instructions:

  1. Create a class Person with attributes first_name and last_name.
  2. Create a class Employee that inherits from Person and has an additional attribute employee_id.
  3. Create a class Student that inherits from Person and has an additional attribute student_id.
  4. Create a class TeachingAssistant that inherits from both Employee and Student.
  5. Define a method get_info() for each class that returns a string with the person's information.
  6. Create an instance of TeachingAssistant and call the get_info() method.

Solution:

class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    def get_info(self):
        return f"{self.first_name} {self.last_name}"

class Employee(Person):
    def __init__(self, first_name, last_name, employee_id):
        super().__init__(first_name, last_name)
        self.employee_id = employee_id

    def get_info(self):
        return f"{super().get_info()}, Employee ID: {self.employee_id}"

class Student(Person):
    def __init__(self, first_name, last_name, student_id):
        super().__init__(first_name, last_name)
        self.student_id = student_id

    def get_info(self):
        return f"{super().get_info()}, Student ID: {self.student_id}"

class TeachingAssistant(Employee, Student):
    def __init__(self, first_name, last_name, employee_id, student_id):
        Employee.__init__(self, first_name, last_name, employee_id)
        Student.__init__(self, first_name, last_name, student_id)

    def get_info(self):
        return f"{super().get_info()}, Employee ID: {self.employee_id}, Student ID: {self.student_id}"

ta1 = TeachingAssistant("John", "Doe", 1001, 2001)
print(ta1.get_info())  # Output: John Doe, Employee ID: 1001, Student ID: 2001