OOP in Python: Part 6 Comparing Multiple Inheritance, Composition, and Aggregation

winstonmhango23

7 min read

Views: 261

Object-Oriented Programming (OOP) is a powerful paradigm in Python that allows developers to model real-world entities and relationships using classes and objects

Object-Oriented Programming (OOP) is a powerful paradigm in Python that allows developers to model real-world entities and relationships using classes and objects. This blog post explores three key concepts in OOP: Multiple Inheritance, Composition, and Aggregation, and how they differ from one another. To illustrate these concepts, we will use examples related to a bank account system.

1.Multiple Inheritance

Theory and Basics

Multiple inheritance allows a class to inherit attributes and methods from more than one parent class. This provides flexibility in designing classes that can combine behaviors from multiple sources. However, it can also introduce complexity, particularly when parent classes have methods or attributes with the same name.

Key Points:

  1. Flexibility: A class can incorporate features and functionalities from multiple classes.
  2. Complexity: Potential for method and attribute conflicts.
  3. Method Resolution Order (MRO): Python uses the MRO to determine which method to invoke, following the C3 linearization algorithm.

Example: Bank Account System

Let's define a complex bank account system using multiple inheritance.

class Account:
    def __init__(self, account_number):
        self.account_number = account_number
        self.balance = 0.0

    def deposit(self, amount):
        if amount <= 0:
            print("Deposit amount must be positive")
            return
        self.balance += amount
        print(f"Deposited {amount:.2f}, new balance is {self.balance:.2f}")

    def withdraw(self, amount):
        if amount <= 0:
            print("Withdrawal amount must be positive")
            return
        if amount > self.balance:
            print("Insufficient funds")
            return
        self.balance -= amount
        print(f"Withdrew {amount:.2f}, new balance is {self.balance:.2f}")

    def get_balance(self):
        return self.balance

    def __str__(self):
        return f"Account Number: {self.account_number}, Balance: {self.balance:.2f}"

class Customer:
    def __init__(self, name, customer_id):
        self.name = name
        self.customer_id = customer_id

    def __str__(self):
        return f"Customer Name: {self.name}, Customer ID: {self.customer_id}"

class InterestBearing:
    def __init__(self, interest_rate):
        self.interest_rate = interest_rate

    def apply_interest(self):
        if hasattr(self, 'balance'):
            self.balance += self.balance * self.interest_rate
            print(f"Interest applied, new balance is {self.balance:.2f}")
        else:
            print("This account does not have a balance attribute")

class SavingsAccount(Account, InterestBearing):
    def __init__(self, account_number, customer, interest_rate):
        Account.__init__(self, account_number)
        InterestBearing.__init__(self, interest_rate)
        self.customer = customer

    def __str__(self):
        return f"{Account.__str__(self)}, {self.customer}, Interest Rate: {self.interest_rate * 100}%"

Usage

customer = Customer("John Doe", "CUST123")
savings_account = SavingsAccount("001", customer, 0.05)

savings_account.deposit(1000)
savings_account.apply_interest()
savings_account.withdraw(500)

print(savings_account)
print(SavingsAccount.__mro__)

2. Composition

Theory and Basics

Composition involves constructing complex objects from simpler ones. It provides more flexibility than inheritance by allowing you to combine objects in various ways. In composition, a class includes instances of other classes as its attributes.

Key Points:

  1. Strong Ownership: The lifecycle of the contained objects is tightly bound to the lifecycle of the containing object.
  2. Encapsulation: Promotes encapsulation by managing the behavior of the contained objects within the containing class.
  3. Flexibility in Behavior: Allows dynamic changes in behavior by replacing contained objects.

Example: Bank Account System

Let's redefine our bank account system using composition.

class Customer:
    def __init__(self, name, customer_id):
        self.name = name
        self.customer_id = customer_id

    def __str__(self):
        return f"Customer Name: {self.name}, Customer ID: {self.customer_id}"

class BankAccount:
    def __init__(self, account_number, customer):
        self.account_number = account_number
        self.customer = customer
        self.balance = 0.0

    def deposit(self, amount):
        if amount <= 0:
            print("Deposit amount must be positive")
            return
        self.balance += amount
        print(f"Deposited {amount:.2f}, new balance is {self.balance:.2f}")

    def withdraw(self, amount):
        if amount <= 0:
            print("Withdrawal amount must be positive")
            return
        if amount > self.balance:
            print("Insufficient funds")
            return
        self.balance -= amount
        print(f"Withdrew {amount:.2f}, new balance is {self.balance:.2f}")

    def get_balance(self):
        return self.balance

    def __str__(self):
        return f"Account Number: {self.account_number}, Balance: {self.balance:.2f}, {self.customer}"

Usage

customer = Customer("John Doe", "CUST123")
account = BankAccount("001", customer)

account.deposit(500)
account.withdraw(200)

print(account)

3. Aggregation

Theory and Basics

Aggregation is a special form of association that represents a "has-a" relationship where the contained objects can exist independently of the parent object. It models a whole-part relationship but with independent lifecycles.

Key Points:

  1. Weak Ownership: Aggregated objects have an independent lifecycle. The parent object does not own the child objects.
  2. Reuse of Objects: Allows the reuse of objects across different parent objects.
  3. Flexibility: Facilitates decoupling and modularity by allowing independent objects to be part of a parent object.

Example: Bank Account System

We'll illustrate aggregation by separating the customer from the bank account, making them independently managed entities.

class Customer:
    def __init__(self, name, customer_id):
        self.name = name
        self.customer_id = customer_id

    def __str__(self):
        return f"Customer Name: {self.name}, Customer ID: {self.customer_id}"

class BankAccount:
    def __init__(self, account_number):
        self.account_number = account_number
        self.balance = 0.0
        self.customer = None  # Customer is an aggregated object

    def set_customer(self, customer):
        self.customer = customer

    def deposit(self, amount):
        if amount <= 0:
            print("Deposit amount must be positive")
            return
        self.balance += amount
        print(f"Deposited {amount:.2f}, new balance is {self.balance:.2f}")

    def withdraw(self, amount):
        if amount <= 0:
            print("Withdrawal amount must be positive")
            return
        if amount > self.balance:
            print("Insufficient funds")
            return
        self.balance -= amount
        print(f"Withdrew {amount:.2f}, new balance is {self.balance:.2f}")

    def get_balance(self):
        return self.balance

    def __str__(self):
        return f"Account Number: {self.account_number}, Balance: {self.balance:.2f}, Customer: {self.customer}"

Usage

customer = Customer("John Doe", "CUST123")
account = BankAccount("001")
account.set_customer(customer)

account.deposit(500)
account.withdraw(200)

print(account)

Conclusion

In OOP, Multiple Inheritance, Composition, and Aggregation each offer different ways to design and manage relationships between classes.

  • Multiple Inheritance: Allows a class to inherit from multiple parent classes, combining features and behaviors, but can introduce complexity and potential conflicts.
  • Composition: Constructs complex objects from simpler ones by including instances of other classes as attributes, promoting strong ownership and encapsulation.
  • Aggregation: Represents a whole-part relationship with independent lifecycles, allowing for the reuse of objects and facilitating decoupling and modularity.

Understanding these concepts and their differences is crucial for designing robust, maintainable, and flexible object-oriented systems. Each approach has its strengths and is suitable for different scenarios, making them invaluable tools in a Python developer's toolkit.

Recent Related Posts

Related Posts

Creating and uploading a Python package to the PyPi: Part 3 Creating The package and uploading to pypi.org

In this part of the series, we will walk you through the final steps of creating a Python package and uploading it to PyPi. We will use the example package bank_creator that we discussed in Part 2. The full code for this package can be found on GitHub.

Read More

Creating and uploading a Python package to the PyPi: Part 2 Creating Git repository and uploading to github

Read More

Creating and uploading a Python package to the PyPi: Part 1 Basics

Read More

© 2024 .