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:
- Flexibility: A class can incorporate features and functionalities from multiple classes.
- Complexity: Potential for method and attribute conflicts.
- 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:
- Strong Ownership: The lifecycle of the contained objects is tightly bound to the lifecycle of the containing object.
- Encapsulation: Promotes encapsulation by managing the behavior of the contained objects within the containing class.
- 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:
- Weak Ownership: Aggregated objects have an independent lifecycle. The parent object does not own the child objects.
- Reuse of Objects: Allows the reuse of objects across different parent objects.
- 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.