Overview
Object-Oriented Programming (OOP) is a programming paradigm that organizes code into objects containing data (attributes) and behaviors (methods). Python supports all OOP concepts with a clean, intuitive syntax.
Classes and Objects
Basic Class Definition
# Simple class
class Dog:
# Class attribute (shared by all instances)
species = "Canis familiaris"
# Constructor (initializer)
def __init__(self, name, age):
# Instance attributes
self.name = name
self.age = age
# Instance method
def bark(self):
return f"{self.name} says Woof!"
# Method with parameters
def describe(self):
return f"{self.name} is {self.age} years old"
# Creating objects (instances)
dog1 = Dog("Max", 3)
dog2 = Dog("Buddy", 5)
print(dog1.bark()) # Max says Woof!
print(dog2.describe()) # Buddy is 5 years old
print(Dog.species) # Canis familiaris
Class vs Instance Attributes
class Counter:
# Class attribute
total_instances = 0
def __init__(self, initial_value=0):
# Instance attributes
self.value = initial_value
# Modify class attribute
Counter.total_instances += 1
def increment(self):
self.value += 1
@classmethod
def get_total_instances(cls):
return cls.total_instances
# Usage
c1 = Counter()
c2 = Counter(10)
c3 = Counter(20)
print(Counter.total_instances) # 3
print(c1.value) # 0
print(c2.value) # 10
Methods Types
Instance, Class, and Static Methods
class MyClass:
class_variable = "shared"
def __init__(self, value):
self.instance_variable = value
# Instance method - access to self
def instance_method(self):
return f"Instance method called on {self.instance_variable}"
# Class method - access to class
@classmethod
def class_method(cls):
return f"Class method called on {cls.class_variable}"
# Static method - no access to self or cls
@staticmethod
def static_method(x, y):
return f"Static method called with {x} and {y}"
# Alternative constructor using class method
@classmethod
def from_string(cls, string_data):
value = int(string_data.split('-')[1])
return cls(value)
# Usage
obj = MyClass(42)
print(obj.instance_method()) # Instance method
print(MyClass.class_method()) # Class method
print(MyClass.static_method(1, 2)) # Static method
# Alternative constructor
obj2 = MyClass.from_string("value-100")
print(obj2.instance_variable) # 100
Inheritance
Single Inheritance
# Base class (parent)
class Animal:
def __init__(self, name, species):
self.name = name
self.species = species
def make_sound(self):
return "Some generic sound"
def describe(self):
return f"{self.name} is a {self.species}"
# Derived class (child)
class Cat(Animal):
def __init__(self, name, breed):
# Call parent constructor
super().__init__(name, "Cat")
self.breed = breed
# Override parent method
def make_sound(self):
return "Meow"
# Add new method
def purr(self):
return f"{self.name} is purring"
# Usage
cat = Cat("Whiskers", "Persian")
print(cat.describe()) # Whiskers is a Cat
print(cat.make_sound()) # Meow
print(cat.purr()) # Whiskers is purring
Multiple Inheritance
# Multiple parent classes
class Flyable:
def __init__(self):
self.altitude = 0
def fly(self):
self.altitude += 100
return f"Flying at {self.altitude} feet"
class Swimmable:
def __init__(self):
self.depth = 0
def swim(self):
self.depth += 10
return f"Swimming at {self.depth} feet deep"
# Multiple inheritance
class Duck(Animal, Flyable, Swimmable):
def __init__(self, name):
Animal.__init__(self, name, "Duck")
Flyable.__init__(self)
Swimmable.__init__(self)
def make_sound(self):
return "Quack"
# Usage
duck = Duck("Donald")
print(duck.make_sound()) # Quack
print(duck.fly()) # Flying at 100 feet
print(duck.swim()) # Swimming at 10 feet deep
# Method Resolution Order (MRO)
print(Duck.__mro__)
Abstract Base Classes
from abc import ABC, abstractmethod
# Abstract base class
class Shape(ABC):
@abstractmethod
def area(self):
"""Calculate area of the shape"""
pass
@abstractmethod
def perimeter(self):
"""Calculate perimeter of the shape"""
pass
def describe(self):
return f"Area: {self.area()}, Perimeter: {self.perimeter()}"
# Concrete implementations
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
import math
return math.pi * self.radius ** 2
def perimeter(self):
import math
return 2 * math.pi * self.radius
# Usage
# shape = Shape() # Error: Can't instantiate abstract class
rectangle = Rectangle(5, 3)
circle = Circle(4)
print(rectangle.describe())
print(circle.describe())
Encapsulation
Private and Protected Members
class BankAccount:
def __init__(self, account_number, initial_balance):
# Public attribute
self.account_number = account_number
# Protected attribute (convention: single underscore)
self._balance = initial_balance
# Private attribute (name mangling: double underscore)
self.__pin = None
# Private method
self.__validate_transaction = lambda amount: amount > 0
# Public method
def deposit(self, amount):
if self.__validate_transaction(amount):
self._balance += amount
return True
return False
# Protected method
def _calculate_interest(self):
return self._balance * 0.05
# Property for controlled access
@property
def balance(self):
"""Read-only access to balance"""
return self._balance
@property
def pin(self):
"""Write-only PIN"""
return "****" # Never reveal actual PIN
@pin.setter
def pin(self, value):
if len(str(value)) == 4:
self.__pin = value
else:
raise ValueError("PIN must be 4 digits")
# Usage
account = BankAccount("123456", 1000)
account.deposit(500)
print(account.balance) # 1500
# account.balance = 2000 # Error: can't set attribute
account.pin = 1234 # Set PIN
print(account.pin) # ****
# print(account.__pin) # Error: no attribute '__pin'
print(account._BankAccount__pin) # 1234 (name mangling)
Polymorphism
Method Overriding and Duck Typing
class AudioFile:
def __init__(self, filename):
self.filename = filename
def play(self):
raise NotImplementedError("Subclass must implement")
class MP3File(AudioFile):
def play(self):
return f"Playing MP3: {self.filename}"
class WAVFile(AudioFile):
def play(self):
return f"Playing WAV: {self.filename}"
class FLACFile(AudioFile):
def play(self):
return f"Playing FLAC: {self.filename}"
# Polymorphic behavior
def play_audio(audio_file):
"""Works with any object that has a play() method"""
return audio_file.play()
# Usage
files = [
MP3File("song.mp3"),
WAVFile("sound.wav"),
FLACFile("music.flac")
]
for file in files:
print(play_audio(file))
# Duck typing example
class RadioStream:
def __init__(self, url):
self.url = url
def play(self):
return f"Streaming from: {self.url}"
# RadioStream also works with play_audio
radio = RadioStream("http://radio.com/stream")
print(play_audio(radio)) # Works due to duck typing
Magic Methods (Dunder Methods)
Common Magic Methods
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
# String representation
def __str__(self):
return f"Vector({self.x}, {self.y})"
def __repr__(self):
return f"Vector(x={self.x}, y={self.y})"
# Arithmetic operations
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
# Comparison operations
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __lt__(self, other):
return self.magnitude() < other.magnitude()
# Container protocol
def __len__(self):
return 2
def __getitem__(self, index):
if index == 0:
return self.x
elif index == 1:
return self.y
raise IndexError("Vector index out of range")
# Callable
def __call__(self):
return self.magnitude()
# Context manager
def __enter__(self):
print(f"Entering context with {self}")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"Exiting context")
# Helper method
def magnitude(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
# Usage
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 + v2) # Vector(4, 6)
print(v1 * 2) # Vector(6, 8)
print(v1 == v2) # False
print(v1[0]) # 3
print(v1()) # 5.0 (magnitude)
with v1 as v:
print(f"In context: {v}")
Properties and Descriptors
Using Properties
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero")
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self.celsius = (value - 32) * 5/9
@property
def kelvin(self):
return self._celsius + 273.15
@kelvin.setter
def kelvin(self, value):
self.celsius = value - 273.15
# Usage
temp = Temperature()
temp.celsius = 25
print(temp.fahrenheit) # 77.0
print(temp.kelvin) # 298.15
temp.fahrenheit = 86
print(temp.celsius) # 30.0
Custom Descriptors
class ValidatedAttribute:
def __init__(self, validator):
self.validator = validator
self.data = {}
def __get__(self, obj, objtype=None):
if obj is None:
return self
return self.data.get(id(obj))
def __set__(self, obj, value):
self.validator(value)
self.data[id(obj)] = value
def __delete__(self, obj):
del self.data[id(obj)]
class Person:
# Descriptors
name = ValidatedAttribute(lambda x: isinstance(x, str))
age = ValidatedAttribute(lambda x: 0 <= x <= 150)
def __init__(self, name, age):
self.name = name
self.age = age
# Usage
person = Person("Alice", 30)
# person.age = 200 # Raises error
Design Patterns
Singleton Pattern
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.initialized = False
return cls._instance
def __init__(self):
if not self.initialized:
self.initialized = True
self.data = {}
def set_data(self, key, value):
self.data[key] = value
def get_data(self, key):
return self.data.get(key)
# Usage
s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # True
s1.set_data("config", "value")
print(s2.get_data("config")) # "value"
Factory Pattern
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):
animals = {
'dog': Dog,
'cat': Cat
}
animal_class = animals.get(animal_type.lower())
if animal_class:
return animal_class()
raise ValueError(f"Unknown animal type: {animal_type}")
# Usage
factory = AnimalFactory()
dog = factory.create_animal('dog')
cat = factory.create_animal('cat')
print(dog.speak()) # Woof!
print(cat.speak()) # Meow!
Observer Pattern
class Subject:
def __init__(self):
self._observers = []
self._state = None
def attach(self, observer):
self._observers.append(observer)
def detach(self, observer):
self._observers.remove(observer)
def notify(self):
for observer in self._observers:
observer.update(self._state)
@property
def state(self):
return self._state
@state.setter
def state(self, value):
self._state = value
self.notify()
class Observer:
def update(self, state):
pass
class ConcreteObserver(Observer):
def __init__(self, name):
self.name = name
def update(self, state):
print(f"{self.name} received update: {state}")
# Usage
subject = Subject()
observer1 = ConcreteObserver("Observer1")
observer2 = ConcreteObserver("Observer2")
subject.attach(observer1)
subject.attach(observer2)
subject.state = "New State" # Both observers get notified
Real-World Example: E-Commerce System
from datetime import datetime
from typing import List, Optional
from abc import ABC, abstractmethod
import uuid
class Product:
def __init__(self, name: str, price: float, stock: int):
self.id = str(uuid.uuid4())
self.name = name
self._price = price
self._stock = stock
@property
def price(self):
return self._price
@price.setter
def price(self, value):
if value < 0:
raise ValueError("Price cannot be negative")
self._price = value
@property
def stock(self):
return self._stock
def reduce_stock(self, quantity):
if quantity > self._stock:
raise ValueError("Insufficient stock")
self._stock -= quantity
def __str__(self):
return f"{self.name} (${self.price}) - Stock: {self.stock}"
class User:
def __init__(self, username: str, email: str):
self.id = str(uuid.uuid4())
self.username = username
self.email = email
self.orders: List['Order'] = []
def place_order(self, order: 'Order'):
self.orders.append(order)
return order.process()
class PaymentMethod(ABC):
@abstractmethod
def process_payment(self, amount: float) -> bool:
pass
class CreditCard(PaymentMethod):
def __init__(self, number: str, cvv: str):
self.number = number
self.cvv = cvv
def process_payment(self, amount: float) -> bool:
# Simulate payment processing
print(f"Processing ${amount} via Credit Card ending in {self.number[-4:]}")
return True
class PayPal(PaymentMethod):
def __init__(self, email: str):
self.email = email
def process_payment(self, amount: float) -> bool:
print(f"Processing ${amount} via PayPal ({self.email})")
return True
class OrderItem:
def __init__(self, product: Product, quantity: int):
self.product = product
self.quantity = quantity
@property
def subtotal(self):
return self.product.price * self.quantity
class Order:
def __init__(self, user: User):
self.id = str(uuid.uuid4())
self.user = user
self.items: List[OrderItem] = []
self.status = "pending"
self.created_at = datetime.now()
self.payment_method: Optional[PaymentMethod] = None
def add_item(self, product: Product, quantity: int):
if product.stock < quantity:
raise ValueError(f"Only {product.stock} items available")
# Check if product already in cart
for item in self.items:
if item.product.id == product.id:
item.quantity += quantity
return
self.items.append(OrderItem(product, quantity))
@property
def total(self):
return sum(item.subtotal for item in self.items)
def set_payment_method(self, payment_method: PaymentMethod):
self.payment_method = payment_method
def process(self):
if not self.payment_method:
raise ValueError("Payment method not set")
if not self.items:
raise ValueError("Order is empty")
# Process payment
if self.payment_method.process_payment(self.total):
# Reduce stock
for item in self.items:
item.product.reduce_stock(item.quantity)
self.status = "completed"
return f"Order {self.id} processed successfully! Total: ${self.total}"
else:
self.status = "failed"
return "Payment failed"
# Usage example
# Create products
laptop = Product("Laptop", 999.99, 10)
mouse = Product("Mouse", 29.99, 50)
# Create user
user = User("john_doe", "john@example.com")
# Create order
order = Order(user)
order.add_item(laptop, 1)
order.add_item(mouse, 2)
# Set payment method
payment = CreditCard("1234567812345678", "123")
order.set_payment_method(payment)
# Process order
result = user.place_order(order)
print(result)
print(f"Laptop stock remaining: {laptop.stock}")
Practice Exercises
Exercise 1: Library Management System
# TODO: Create a library management system
"""
Requirements:
- Book class with ISBN, title, author, available copies
- Member class with membership ID, borrowed books
- Library class managing books and members
- Borrowing and returning functionality
- Fine calculation for late returns
- Search functionality
"""
class Book:
# Your implementation here
pass
class Member:
# Your implementation here
pass
class Library:
# Your implementation here
pass
# Test your implementation
Exercise 2: Game Character System
# TODO: Create a game character system with inheritance
"""
Requirements:
- Base Character class with health, attack, defense
- Warrior, Mage, Archer subclasses with unique abilities
- Equipment system affecting stats
- Battle system between characters
- Level up mechanism
"""
class Character:
# Your implementation here
pass
# Test with battle simulation
Exercise 3: Banking System
# TODO: Build a banking system with multiple account types
"""
Requirements:
- Account base class
- Savings, Checking, Investment account types
- Transaction history
- Interest calculation
- Transfer between accounts
- Account statements
"""
class Account:
# Your implementation here
pass
# Test with various transactions
Best Practices
1. Follow naming conventions: CamelCase for classes, snake_case for methods 2. Keep classes focused: Single Responsibility Principle 3. Use inheritance wisely: Prefer composition over inheritance 4. Document classes with docstrings 5. Make attributes private when appropriate 6. Use properties for controlled access 7. Implement useful magic methods for better usability 8. Follow SOLID principles for maintainable code 9. Use abstract base classes for interfaces 10. Test your classes thoroughly
Common Pitfalls
# Pitfall 1: Mutable class attributes
class BadExample:
shared_list = [] # Shared by all instances!
# Correct approach
class GoodExample:
def __init__(self):
self.instance_list = [] # Unique to each instance
# Pitfall 2: Not calling super().__init__()
class Parent:
def __init__(self):
self.important_setup = True
class Child(Parent):
def __init__(self):
# super().__init__() # Forgot this!
self.child_attr = "value"
# Pitfall 3: Modifying list during iteration
class Collection:
def __init__(self):
self.items = []
def remove_items(self, condition):
# Wrong way
for item in self.items:
if condition(item):
self.items.remove(item) # Modifies during iteration!
# Correct way
self.items = [item for item in self.items if not condition(item)]