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 name, age, 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 barksInheritance 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:
- Create a base class called Vehiclewith attributesmake,model, andyear.
- Define a method vehicle_info()that prints the make, model, and year of the vehicle.
- Create a class Carthat inherits fromVehicleand has an additional attributedoors.
- Create a class Motorcyclethat inherits fromVehicleand has an additional attributetype.
- Create instances of CarandMotorcycle, and call thevehicle_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 R1Exercise 8.3.2: Inheritance and Method Overriding
Create classes representing different types of bank accounts, using inheritance and method overriding.
Instructions:
- Create a base class BankAccountwith attributesbalanceand a methoddeposit().
- Define a method withdraw()that checks if the withdrawal amount is less than or equal to the balance, and updates the balance accordingly.
- Create a class SavingsAccountthat inherits fromBankAccountand has an additional attributeinterest_rate.
- Override the withdraw()method inSavingsAccountto include a withdrawal fee of 1% of the withdrawal amount.
- Create instances of BankAccountandSavingsAccount, and test thedeposit()andwithdraw()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.5Exercise 8.3.3: Multiple Inheritance
Implement a class hierarchy using multiple inheritance.
Instructions:
- Create a class Personwith attributesfirst_nameandlast_name.
- Create a class Employeethat inherits fromPersonand has an additional attributeemployee_id.
- Create a class Studentthat inherits fromPersonand has an additional attributestudent_id.
- Create a class TeachingAssistantthat inherits from bothEmployeeandStudent.
- Define a method get_info()for each class that returns a string with the person's information.
- Create an instance of TeachingAssistantand call theget_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: 20018.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 name, age, 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 barksInheritance 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:
- Create a base class called Vehiclewith attributesmake,model, andyear.
- Define a method vehicle_info()that prints the make, model, and year of the vehicle.
- Create a class Carthat inherits fromVehicleand has an additional attributedoors.
- Create a class Motorcyclethat inherits fromVehicleand has an additional attributetype.
- Create instances of CarandMotorcycle, and call thevehicle_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 R1Exercise 8.3.2: Inheritance and Method Overriding
Create classes representing different types of bank accounts, using inheritance and method overriding.
Instructions:
- Create a base class BankAccountwith attributesbalanceand a methoddeposit().
- Define a method withdraw()that checks if the withdrawal amount is less than or equal to the balance, and updates the balance accordingly.
- Create a class SavingsAccountthat inherits fromBankAccountand has an additional attributeinterest_rate.
- Override the withdraw()method inSavingsAccountto include a withdrawal fee of 1% of the withdrawal amount.
- Create instances of BankAccountandSavingsAccount, and test thedeposit()andwithdraw()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.5Exercise 8.3.3: Multiple Inheritance
Implement a class hierarchy using multiple inheritance.
Instructions:
- Create a class Personwith attributesfirst_nameandlast_name.
- Create a class Employeethat inherits fromPersonand has an additional attributeemployee_id.
- Create a class Studentthat inherits fromPersonand has an additional attributestudent_id.
- Create a class TeachingAssistantthat inherits from bothEmployeeandStudent.
- Define a method get_info()for each class that returns a string with the person's information.
- Create an instance of TeachingAssistantand call theget_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: 20018.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 name, age, 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 barksInheritance 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:
- Create a base class called Vehiclewith attributesmake,model, andyear.
- Define a method vehicle_info()that prints the make, model, and year of the vehicle.
- Create a class Carthat inherits fromVehicleand has an additional attributedoors.
- Create a class Motorcyclethat inherits fromVehicleand has an additional attributetype.
- Create instances of CarandMotorcycle, and call thevehicle_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 R1Exercise 8.3.2: Inheritance and Method Overriding
Create classes representing different types of bank accounts, using inheritance and method overriding.
Instructions:
- Create a base class BankAccountwith attributesbalanceand a methoddeposit().
- Define a method withdraw()that checks if the withdrawal amount is less than or equal to the balance, and updates the balance accordingly.
- Create a class SavingsAccountthat inherits fromBankAccountand has an additional attributeinterest_rate.
- Override the withdraw()method inSavingsAccountto include a withdrawal fee of 1% of the withdrawal amount.
- Create instances of BankAccountandSavingsAccount, and test thedeposit()andwithdraw()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.5Exercise 8.3.3: Multiple Inheritance
Implement a class hierarchy using multiple inheritance.
Instructions:
- Create a class Personwith attributesfirst_nameandlast_name.
- Create a class Employeethat inherits fromPersonand has an additional attributeemployee_id.
- Create a class Studentthat inherits fromPersonand has an additional attributestudent_id.
- Create a class TeachingAssistantthat inherits from bothEmployeeandStudent.
- Define a method get_info()for each class that returns a string with the person's information.
- Create an instance of TeachingAssistantand call theget_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: 20018.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 name, age, 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 barksInheritance 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:
- Create a base class called Vehiclewith attributesmake,model, andyear.
- Define a method vehicle_info()that prints the make, model, and year of the vehicle.
- Create a class Carthat inherits fromVehicleand has an additional attributedoors.
- Create a class Motorcyclethat inherits fromVehicleand has an additional attributetype.
- Create instances of CarandMotorcycle, and call thevehicle_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 R1Exercise 8.3.2: Inheritance and Method Overriding
Create classes representing different types of bank accounts, using inheritance and method overriding.
Instructions:
- Create a base class BankAccountwith attributesbalanceand a methoddeposit().
- Define a method withdraw()that checks if the withdrawal amount is less than or equal to the balance, and updates the balance accordingly.
- Create a class SavingsAccountthat inherits fromBankAccountand has an additional attributeinterest_rate.
- Override the withdraw()method inSavingsAccountto include a withdrawal fee of 1% of the withdrawal amount.
- Create instances of BankAccountandSavingsAccount, and test thedeposit()andwithdraw()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.5Exercise 8.3.3: Multiple Inheritance
Implement a class hierarchy using multiple inheritance.
Instructions:
- Create a class Personwith attributesfirst_nameandlast_name.
- Create a class Employeethat inherits fromPersonand has an additional attributeemployee_id.
- Create a class Studentthat inherits fromPersonand has an additional attributestudent_id.
- Create a class TeachingAssistantthat inherits from bothEmployeeandStudent.
- Define a method get_info()for each class that returns a string with the person's information.
- Create an instance of TeachingAssistantand call theget_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
