Object-Oriented Programming is a paradigm where you organize code around objects that combine data and behavior. OOP makes complex programs easier to build, understand, and maintain.
Without OOP:
# Scattered data
player1_name = "Alice"
player1_health = 100
player1_inventory = []
player2_name = "Bob"
player2_health = 100
player2_inventory = []
Functions that need to know structure
def take_damage(name, health, amount):
health -= amount
return healthWith OOP:
class Player:
def __init__(self, name):
self.name = name
self.health = 100
self.inventory = []
def take_damage(self, amount):
self.health -= amount
player1 = Player("Alice")
player2 = Player("Bob")
player1.take_damage(20) # Clean and intuitiveA class is a blueprint. An object is an instance of that class.
class Dog:
pass # Empty class for now
Create objects (instances)
dog1 = Dog()
dog2 = Dog()
print(type(dog1)) # <class '__main__.Dog'>
print(dog1 == dog2) # False - different objectsInitialize object attributes when created:
class Dog:
def __init__(self, name, age):
"""Constructor - called when creating new Dog."""
self.name = name # Instance attribute
self.age = age
self.tricks = []
Create dogs
buddy = Dog("Buddy", 3)
max_dog = Dog("Max", 5)
print(buddy.name) # Buddy
print(max_dog.age) # 5self refers to the instance being created/used.
Methods are functions that belong to objects:
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
"""Add money to account."""
if amount > 0:
self.balance += amount
print(f"Deposited ${amount}. New balance: ${self.balance}")
def withdraw(self, amount):
"""Remove money from account."""
if amount > self.balance:
print("Insufficient funds!")
else:
self.balance -= amount
print(f"Withdrew ${amount}. New balance: ${self.balance}")
def get_balance(self):
"""Return current balance."""
return self.balance
Use it
account = BankAccount("Alice", 1000)
account.deposit(500) # Deposited $500. New balance: $1500
account.withdraw(200) # Withdrew $200. New balance: $1300
print(account.get_balance()) # 1300class Dog:
# Class attribute (shared by ALL dogs)
species = "Canis familiaris"
def __init__(self, name, age):
# Instance attributes (unique to each dog)
self.name = name
self.age = agebuddy = Dog("Buddy", 3)
max_dog = Dog("Max", 5)
print(buddy.species) # Canis familiaris
print(max_dog.species) # Canis familiaris
Change class attribute
Dog.species = "Canis lupus familiaris"
print(buddy.species) # Changed for all instances!class Pizza:
def __init__(self, size, toppings):
self.size = size
self.toppings = toppings
@classmethod
def margherita(cls):
"""Factory method for margherita pizza."""
return cls("medium", ["mozzarella", "tomato", "basil"])
@classmethod
def pepperoni(cls):
"""Factory method for pepperoni pizza."""
return cls("large", ["mozzarella", "pepperoni"])
@staticmethod
def is_valid_size(size):
"""Utility function - doesn't need instance or class."""
return size in ["small", "medium", "large"]
Use factory methods
pizza1 = Pizza.margherita()
pizza2 = Pizza.pepperoni()
Use static method
print(Pizza.is_valid_size("medium")) # True
print(Pizza.is_valid_size("jumbo")) # FalseCreate specialized classes from general ones:
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "Some sound"
class Dog(Animal):
def speak(self):
return f"{self.name} says Woof!"
class Cat(Animal):
def speak(self):
return f"{self.name} says Meow!"
Create instances
dog = Dog("Buddy")
cat = Cat("Whiskers")
print(dog.speak()) # Buddy says Woof!
print(cat.speak()) # Whiskers says Meow!Call parent class methods:
class Vehicle:
def __init__(self, brand, model):
self.brand = brand
self.model = model
def describe(self):
return f"{self.brand} {self.model}"
class Car(Vehicle):
def __init__(self, brand, model, doors):
super().__init__(brand, model) # Call parent constructor
self.doors = doors
def describe(self):
parent_desc = super().describe() # Call parent method
return f"{parent_desc} with {self.doors} doors"
car = Car("Toyota", "Camry", 4)
print(car.describe()) # Toyota Camry with 4 doorsUse leading underscore for "private" attributes:
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self._balance = balance # "Private" by convention
def deposit(self, amount):
if amount > 0:
self._balance += amount
def get_balance(self):
return self._balance
# Don't allow direct modification
# account._balance = 1000000 # Bad practice but possible
account = BankAccount("Alice", 1000)
Use method instead of direct access
account.deposit(500)
print(account.get_balance())Python has no true private attributes - leading underscore is just a convention saying "don't touch this directly."
Make attributes that act like methods:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
"""Get temperature in Celsius."""
return self._celsius
@celsius.setter
def celsius(self, value):
"""Set temperature in Celsius with validation."""
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
self._celsius = value
@property
def fahrenheit(self):
"""Get temperature in Fahrenheit."""
return (self._celsius * 9/5) + 32
@fahrenheit.setter
def fahrenheit(self, value):
"""Set temperature using Fahrenheit."""
self.celsius = (value - 32) * 5/9
Use like attributes
temp = Temperature(25)
print(temp.celsius) # 25
print(temp.fahrenheit) # 77.0
temp.fahrenheit = 86 # Sets via property
print(temp.celsius) # 30.0
# temp.celsius = -300 # Raises ValueErrorSpecial methods that Python calls automatically:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
"""Called by str() and print()."""
return f"Point({self.x}, {self.y})"
def __repr__(self):
"""Called by repr() and in interactive shell."""
return f"Point(x={self.x}, y={self.y})"
def __add__(self, other):
"""Called by + operator."""
return Point(self.x + other.x, self.y + other.y)
def __eq__(self, other):
"""Called by == operator."""
return self.x == other.x and self.y == other.y
def __len__(self):
"""Called by len()."""
return int((self.x**2 + self.y**2) ** 0.5)
p1 = Point(1, 2)
p2 = Point(3, 4)
print(p1) # Point(1, 2)
print(p1 + p2) # Point(4, 6)
print(p1 == p2) # False
print(len(p1)) # 2 (distance from origin)Common magic methods:
__init__ - constructor__str__ - string representation__repr__ - official representation__add__, __sub__, __mul__ - arithmetic__eq__, __lt__, __gt__ - comparisons__len__ - length__getitem__ - indexing obj[key]__setitem__ - assignment obj[key] = valSometimes it's better to have something than be something:
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
def start(self):
return "Engine started"
class Car:
def __init__(self, brand, engine):
self.brand = brand
self.engine = engine # Composition - Car HAS an Engine
def start(self):
return f"{self.brand}: {self.engine.start()}"
Create car with engine
engine = Engine(200)
car = Car("Toyota", engine)
print(car.start()) # Toyota: Engine startedWhen to use each:
class Character:
def __init__(self, name, health, attack, defense):
self.name = name
self.max_health = health
self.health = health
self.attack = attack
self.defense = defense
self.level = 1
def take_damage(self, damage):
actual_damage = max(0, damage - self.defense)
self.health = max(0, self.health - actual_damage)
if self.health == 0:
print(f"{self.name} has been defeated!")
else:
print(f"{self.name} took {actual_damage} damage! HP: {self.health}/{self.max_health}")
def heal(self, amount):
old_health = self.health
self.health = min(self.max_health, self.health + amount)
healed = self.health - old_health
print(f"{self.name} healed {healed} HP! HP: {self.health}/{self.max_health}")
def is_alive(self):
return self.health > 0
class Warrior(Character):
def __init__(self, name):
super().__init__(name, health=150, attack=30, defense=20)
self.rage = 0
def special_attack(self, target):
damage = self.attack + self.rage
print(f"{self.name} uses Rage Strike for {damage} damage!")
target.take_damage(damage)
self.rage = min(100, self.rage + 10)
class Mage(Character):
def __init__(self, name):
super().__init__(name, health=80, attack=50, defense=5)
self.mana = 100
def special_attack(self, target):
if self.mana >= 30:
damage = self.attack * 2
print(f"{self.name} casts Fireball for {damage} damage!")
target.take_damage(damage)
self.mana -= 30
else:
print(f"{self.name} has insufficient mana!")
class Healer(Character):
def __init__(self, name):
super().__init__(name, health=100, attack=15, defense=10)
self.mana = 150
def special_attack(self, target):
if self.mana >= 20:
target.heal(40)
self.mana -= 20
else:
print(f"{self.name} has insufficient mana!")
Create party
warrior = Warrior("Conan")
mage = Mage("Gandalf")
healer = Healer("Elara")
Battle simulation
enemy = Character("Dragon", 200, 40, 15)
warrior.special_attack(enemy)
mage.special_attack(enemy)
enemy.take_damage(warrior.attack)
healer.special_attack(warrior)class Animal:
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
class AnimalFactory:
@staticmethod
def create_animal(animal_type):
if animal_type == "dog":
return Dog()
elif animal_type == "cat":
return Cat()
else:
raise ValueError(f"Unknown animal type: {animal_type}")
Use factory
animal = AnimalFactory.create_animal("dog")
print(animal.speak()) # Woof!Book class with title, author, and pagesLibrary class that manages a collection of booksShape parent class with Circle and Rectangle childrenStack class with push/pop/peek methodsTodoList class with add/remove/complete functionality__init__ initializes object attributesself refers to the current instancesuper() calls parent class methods