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 = balance
Task 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,000
Solution β
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 attribute
Accessing 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 variable
Class Methods β
Concept: Methods that operate on the class itself rather than instances.
@classmethod
def update_interest_rate(cls, new_rate):
cls.interest_rate = new_rate
Static 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_rate
Task 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_rate
class variable and adjust theapply_interest
method accordingly. - In
CheckingAccount
, we override thewithdraw
method 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_rate
andself.transaction_fee
that 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_bonus
method to showcase adding new functionality. - In
CheckingAccount
, we introduce anorder_checks
method 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,940
Magic 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 NotImplemented
Example:
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) # AttributeError
Solution: 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 = speed
Solution: 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!