Embracing Object-Oriented Programming π¦ΈββοΈπ° β
Greetings, aspiring heroes! Today, we embark on a journey into the realm of Object-Oriented Programming (OOP). Just as superheroes have unique abilities and identities, OOP allows us to model real-world entities in our code. Let's dive in and discover how classes and objects can empower your Python programs.
The Many Paradigms of Programming π§ β
Question: What are the different styles or paradigms of programming?
- Procedural Programming: Writing procedures or functions that perform operations on data.
- Functional Programming: Building programs using pure functions, avoiding shared state.
- Object-Oriented Programming: Organizing code into objects that combine data and behavior.
- Mathematical Programming: Using mathematical constructs and logic to solve problems.
Today, we'll focus on Object-Oriented Programming.
Modeling the Supercar: Introducing Classes and Objects π β
Imagine you're tasked with designing a fleet of superhero vehicles.
Defining a Car Without OOP β
def car(name, engine, wheels, doors):
return {"name": name, "engine": engine, "wheels": wheels, "doors": doors}
print(car("Ferrari", "V8", 4, 2))Output:
{'name': 'Ferrari', 'engine': 'V8', 'wheels': 4, 'doors': 2}Question: What's missing when we represent a car as a simple dictionary?
- Lack of behavior (methods) associated with the car.
- No encapsulation of data and functions.
Introducing the Car Class β
Concept: A class is a blueprint for creating objects. It defines attributes (data) and methods (behavior).
class Car:
def __init__(self, name, engine, wheels, doors):
self.name = name
self.engine = engine
self.wheels = wheels
self.doors = doors
def horn(self):
return f"{self.name} says Vroom, Vroom π―"__init__: The constructor method called when creating a new object.self: Refers to the instance of the class.
Creating Car Objects (Instances) β
ferrari = Car("Ferrari", "V8", 4, 2)
toyota = Car("Toyota", "V4", 4, 4)
print(toyota.horn())
print(ferrari.horn())Output:
Toyota says Vroom, Vroom π―
Ferrari says Vroom, Vroom π―Task 1: Building a Super Bank π¦ β
Our superheroes need a secure place to manage their finances.
Task 1.1: Create a BankAccount Class β
Attributes:
acc_no: Account numbername: Account holder's namebalance: Account balance
Task
Define a BankAccount class with the above attributes.
Solution β
Answer
class BankAccount:
def __init__(self, acc_no, name, balance):
self.acc_no = acc_no
self.name = name
self.balance = balanceTask 1.2: Create Three Bank Account Objects β
Task
Create three instances of BankAccount for different superheroes.
Solution β
Answer
account1 = BankAccount(101, "Iron Man", 1_000_000)
account2 = BankAccount(102, "Captain America", 500_000)
account3 = BankAccount(103, "Black Widow", 750_000)Enhancing the Bank with Methods π³ β
Our heroes need to perform transactions on their accounts.
Task 2.1: Implement display_balance Method β
Objective: Display the account balance in a user-friendly format.
Task
Add a method display_balance to BankAccount that returns a string like:
Iron Man, your balance is βΉ1,000,000Solution β
Answer
class BankAccount:
def __init__(self, acc_no, name, balance):
self.acc_no = acc_no
self.name = name
self.balance = balance
def display_balance(self):
return f"{self.name}, your balance is βΉ{self.balance:,}"Task 2.2: Implement withdraw Method β
Objective: Allow superheroes to withdraw money from their accounts.
Task
Add a method withdraw that subtracts the amount from the balance if sufficient funds are available.
Solution β
Answer
class BankAccount:
# ... [previous code] ...
def withdraw(self, amount):
if amount > self.balance:
return f"Insufficient funds. {self.display_balance()}"
self.balance -= amount
return f"Transaction successful. {self.display_balance()}"Task 2.3: Implement deposit Method β
Objective: Allow superheroes to deposit money into their accounts.
Task
Add a method deposit that adds the amount to the balance.
Solution β
Answer
class BankAccount:
# ... [previous code] ...
def deposit(self, amount):
if amount <= 0:
return "Please provide a valid amount."
self.balance += amount
return f"Successfully deposited. {self.display_balance()}"Encapsulation: Protecting Our Assets π β
To prevent unauthorized access, we need to secure our bank accounts.
Private Attributes β
Concept: In Python, attributes prefixed with __ (double underscores) are considered private.
class SecureBankAccount:
def __init__(self, acc_no, name, balance):
self.acc_no = acc_no
self.name = name
self.__balance = balance # Private attributeAccessing Private Attributes β
Question: How can we access or modify private attributes?
- Through methods that control access (getters and setters).
- Direct access is discouraged to maintain encapsulation.
Class Variables and Methods ποΈ β
Some properties are shared across all accounts, like the bank's interest rate.
Class Variables β
class Bank:
interest_rate = 0.02 # Class variable
no_of_accounts = 0
def __init__(self, acc_no, name, balance):
self.acc_no = acc_no
self.name = name
self.__balance = balance
Bank.no_of_accounts += 1 # Updating class variableClass Methods β
Concept: Methods that operate on the class itself rather than instances.
@classmethod
def update_interest_rate(cls, new_rate):
cls.interest_rate = new_rateStatic Methods β
Concept: Methods that do not access instance or class variables.
@staticmethod
def bank_info():
return "Welcome to Super Bank!"Task 3: Applying Interest and Tracking Accounts π β
Task 3.1: Implement apply_interest Method β
Objective: Apply interest to the account balance.
Task
Add a method apply_interest that increases the balance based on the interest rate.
Solution β
Answer
class BankAccount:
interest_rate = 0.02
# ... [previous code] ...
def apply_interest(self):
self.balance += self.balance * BankAccount.interest_rateTask 3.2: Display Total Number of Accounts β
Objective: Keep track of how many accounts have been created.
Task
Use a class variable no_of_accounts and a method to display the total number of accounts.
Solution β
Answer
class BankAccount:
no_of_accounts = 0
# ... [previous code] ...
def __init__(self, acc_no, name, balance):
self.acc_no = acc_no
self.name = name
self.balance = balance
BankAccount.no_of_accounts += 1
@staticmethod
def display_total_accounts():
return f"Total accounts in the bank: {BankAccount.no_of_accounts}"Inheritance: Powering Up with Super Classes π¦ΈββοΈ β
Superheroes often have abilities inherited from others.
Defining a Base Class Animal β
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "Some generic sound"Creating a Derived Class Dog β
class Dog(Animal):
def speak(self):
return "Woof Woof!! π"
def run(self):
return "πΆ wags tail!!"Using the Classes β
toby = Animal("Toby")
maxy = Dog("Maxy")
print(toby.speak()) # Output: Some generic sound
print(maxy.speak()) # Output: Woof Woof!! π
print(maxy.run()) # Output: πΆ wags tail!!Method Overriding and Super Calls π¦ΉββοΈ β
Question: What if we want to extend the __init__ method in a subclass?
Extending Initialization β
class Dog(Animal):
def __init__(self, name, speed):
super().__init__(name)
self.speed = speed
def speak(self):
return "Woof Woof!! π"
def run(self):
return f"πΆ runs at {self.speed} km/h!"Tip: Use super().__init__(...) to call the parent class's constructor.
Task 4: Specialized Bank Accounts πΌ β
Our superheroes need different types of accounts.
Task: Create SavingsAccount and CheckingAccount β
- SavingsAccount: Higher interest rate (e.g., 5%).
- CheckingAccount: Transaction fee for withdrawals.
Task
Create subclasses SavingsAccount and CheckingAccount with specific behaviors.
Solution 1: Overriding Class Variables β
Answer
class SavingsAccount(BankAccount):
interest_rate = 0.05 # Overriding the class variable
def apply_interest(self):
self.balance += self.balance * SavingsAccount.interest_rate
class CheckingAccount(BankAccount):
transaction_fee = 10
def withdraw(self, amount):
total_amount = amount + CheckingAccount.transaction_fee
return super().withdraw(total_amount)Explanation:
- In
SavingsAccount, we override theinterest_rateclass variable and adjust theapply_interestmethod accordingly. - In
CheckingAccount, we override thewithdrawmethod to include a transaction fee.
Solution 2: Using Initialization Parameters β
Answer
class SavingsAccount(BankAccount):
def __init__(self, acc_no, name, balance, interest_rate=0.05):
super().__init__(acc_no, name, balance)
self.interest_rate = interest_rate
def apply_interest(self):
self.balance += self.balance * self.interest_rate
class CheckingAccount(BankAccount):
def __init__(self, acc_no, name, balance, transaction_fee=10):
super().__init__(acc_no, name, balance)
self.transaction_fee = transaction_fee
def withdraw(self, amount):
total_amount = amount + self.transaction_fee
return super().withdraw(total_amount)Explanation:
- We introduce instance variables
self.interest_rateandself.transaction_feethat can be set during initialization. - This allows for more flexibility if different accounts have different rates or fees.
Solution 3: Implementing Additional Methods β
Answer
class SavingsAccount(BankAccount):
interest_rate = 0.05
def apply_interest(self):
super().apply_interest() # Uses the parent method, but with overridden interest_rate
def add_bonus(self, bonus_amount):
self.balance += bonus_amount
return f"Bonus added! {self.display_balance()}"
class CheckingAccount(BankAccount):
transaction_fee = 10
def withdraw(self, amount):
total_amount = amount + CheckingAccount.transaction_fee
return super().withdraw(total_amount)
def order_checks(self):
fee = 50 # Fixed fee for ordering checks
if self.balance >= fee:
self.balance -= fee
return f"Checks ordered successfully. {self.display_balance()}"
else:
return f"Insufficient funds to order checks. {self.display_balance()}"Explanation:
- In
SavingsAccount, we usesuper().apply_interest()to utilize the parent's method but still benefit from the overriddeninterest_rate. - We add an
add_bonusmethod to showcase adding new functionality. - In
CheckingAccount, we introduce anorder_checksmethod to demonstrate additional behaviors specific to checking accounts.
Using the Specialized Accounts β
# Creating instances
savings = SavingsAccount(201, "Thor", 200_000)
checking = CheckingAccount(202, "Loki", 150_000)
# Applying interest
savings.apply_interest()
print(savings.display_balance()) # Should reflect the higher interest rate
# Withdrawing with transaction fee
print(checking.withdraw(50_000)) # Should deduct amount plus transaction fee
# Ordering checks
print(checking.order_checks())Sample Output:
Thor, your balance is βΉ210,000.0
Transaction successful. Loki, your balance is βΉ99,990
Checks ordered successfully. Loki, your balance is βΉ99,940Magic Methods: Adding Special Powers β¨ β
Magic methods allow objects to interact with Python's built-in functions.
The __str__ and __repr__ Methods β
Question: How can we define how our objects are printed?
class BankAccount:
# ... [previous code] ...
def __str__(self):
return f"Account({self.acc_no}): {self.name} with balance βΉ{self.balance:,}"
def __repr__(self):
return f"BankAccount({self.acc_no}, '{self.name}', {self.balance})"The __add__ Method β
Question: Can we define custom behavior for the + operator?
def __add__(self, other):
if isinstance(other, BankAccount):
return self.balance + other.balance
return NotImplementedExample:
total_balance = account1 + account2
print(f"Total balance: βΉ{total_balance:,}")Pitfalls and Best Practices π§ β
Accessing Private Attributes β
Pitfall
Attempting to access private attributes directly can lead to errors.
print(account1.__balance) # AttributeErrorSolution: Use methods to access private data.
Method Overriding β
Pitfall
Forgetting to call super().__init__() in a subclass can result in missing initializations.
class Dog(Animal):
def __init__(self, name, speed):
self.name = name # Missing super().__init__(name)
self.speed = speedSolution: Always call super().__init__() when overriding __init__.
Conclusion π β
By embracing Object-Oriented Programming, you've unlocked the ability to model complex systems in a way that mirrors the real world. Classes and objects allow you to encapsulate data and behavior, promote code reuse through inheritance, and create more maintainable and scalable programs.
Farewell, Aspiring Hero! π β
Your journey into the realm of OOP has added powerful tools to your coding arsenal. Continue to explore and practice these concepts, and you'll be well on your way to becoming a Python superhero!