Module 12: OOPBuilding with blueprints
Object-Oriented Programming is a way of organizing code around objects — bundles of related data and behavior. Instead of writing a long list of disconnected functions and variables, OOP lets you model real-world things: a User, a Product, a BankAccount, a Vehicle.
Think of a class as a blueprint and an object as a building made from that blueprint. A hundred houses can be built from the same blueprint — each is its own structure, but they share the same design.
Classes & Objects
class BankAccount:
# Constructor — runs when a new account is created
def __init__(self, owner, balance=0):
self.owner = owner # Instance attribute
self.balance = balance
def deposit(self, amount):
self.balance += amount
print(f'Deposited GHS {amount}. New balance: GHS {self.balance}')
def withdraw(self, amount):
if amount > self.balance:
print('Insufficient funds.')
else:
self.balance -= amount
print(f'Withdrew GHS {amount}. Remaining: GHS {self.balance}')
def get_balance(self):
return self.balance
# Creating objects (instances)
account1 = BankAccount('Ama Mensah', 1000)
account2 = BankAccount('Kofi Boateng')
account1.deposit(500) # Deposited GHS 500. New balance: GHS 1500
account1.withdraw(200) # Withdrew GHS 200. Remaining: GHS 1300
account2.deposit(300) # Deposited GHS 300. New balance: GHS 300
Encapsulation
Encapsulation means bundling data and the methods that operate on that data inside a class, and controlling access from outside. In Python, we signal private attributes with a leading underscore:
class Employee:
def __init__(self, name, salary):
self.name = name
self._salary = salary # 'Private' by convention
def get_salary(self):
return self._salary
def give_raise(self, percent):
if percent > 0:
self._salary *= (1 + percent/100)
else:
print('Raise must be positive.')
emp = Employee('Abena', 5000)
emp.give_raise(10) # Proper way
print(emp.get_salary()) # 5500.0
# emp._salary = -1000 ← Wrong! Bypasses validation
Inheritance
Inheritance lets one class (child) inherit attributes and methods from another class (parent). This prevents code duplication and models real-world hierarchies:
class Animal:
def __init__(self, name, sound):
self.name = name
self.sound = sound
def speak(self):
return f'{self.name} says {self.sound}!'
class Dog(Animal): # Dog inherits from Animal
def __init__(self, name):
super().__init__(name, 'Woof') # Call parent constructor
def fetch(self, item):
return f'{self.name} fetches the {item}!'
class Cat(Animal):
def __init__(self, name):
super().__init__(name, 'Meow')
dog = Dog('Rex')
cat = Cat('Whiskers')
print(dog.speak()) # Rex says Woof!
print(cat.speak()) # Whiskers says Meow!
print(dog.fetch('ball')) # Rex fetches the ball!
Polymorphism
Polymorphism means different objects can respond to the same method call in their own way. The calling code doesn't need to know what type of object it's dealing with:
class Shape:
def area(self):
return 0
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
shapes = [Circle(5), Rectangle(4, 6), Circle(3)]
for shape in shapes:
print(f'Area: {shape.area():.2f}') # Same call, different results