Object-Oriented Programming and Design Patterns
70 minPython's OOP implementation is based on the principle that everything is an object. Classes are blueprints for creating objects, and objects are instances of classes with their own state and behavior.
Python supports multiple inheritance, method resolution order (MRO), and special methods (dunder methods) that enable powerful object-oriented patterns. Understanding these concepts is crucial for advanced Python development.
Design patterns provide proven solutions to common programming problems. Python's dynamic nature makes it particularly well-suited for implementing patterns like Singleton, Factory, Observer, and Strategy.
Modern Python development emphasizes composition over inheritance, and the use of abstract base classes and protocols for better code design and maintainability.
Python's special methods (__init__, __str__, __repr__, __eq__, etc.) allow you to define how objects behave with built-in operations, making your classes feel native to Python.
The Method Resolution Order (MRO) determines how Python searches for methods in inheritance hierarchies, especially important when dealing with multiple inheritance scenarios.
Key Concepts
- Classes are blueprints for creating objects with state and behavior.
- Inheritance allows classes to inherit attributes and methods from parent classes.
- Encapsulation hides internal implementation details from external code.
- Polymorphism allows different classes to be used interchangeably.
- Special methods (dunder methods) customize object behavior.
Learning Objectives
Master
- Creating classes and instantiating objects
- Understanding inheritance and method resolution order
- Implementing special methods for custom behavior
- Applying design patterns in Python
Develop
- Object-oriented design thinking
- Understanding when to use composition vs inheritance
- Recognizing and applying design patterns
Tips
- Prefer composition over inheritance for more flexible designs.
- Use dataclasses for simple data containers (Python 3.7+).
- Implement __repr__ for better debugging and __str__ for user-friendly output.
- Use abstract base classes (ABC) to define interfaces.
Common Pitfalls
- Overusing inheritance when composition would be better.
- Not understanding MRO, causing unexpected method resolution.
- Forgetting to call super() in child class constructors.
- Creating overly complex inheritance hierarchies.
Summary
- OOP in Python enables code organization and reusability.
- Inheritance allows code reuse but composition is often preferred.
- Special methods customize how objects interact with Python operations.
- Design patterns provide proven solutions to common problems.
Exercise
Implement a comprehensive OOP demonstration that showcases inheritance, polymorphism, design patterns, and best practices for object-oriented design.
from abc import ABC, abstractmethod
from typing import List, Dict, Any, Optional
from dataclasses import dataclass
from enum import Enum
import json
class PaymentMethod(Enum):
"""Enumeration for different payment methods."""
CREDIT_CARD = "credit_card"
DEBIT_CARD = "debit_card"
BANK_TRANSFER = "bank_transfer"
CASH = "cash"
class PaymentProcessor(ABC):
"""Abstract base class for payment processors."""
@abstractmethod
def process_payment(self, amount: float) -> bool:
"""Process a payment and return success status."""
pass
@abstractmethod
def get_processing_fee(self) -> float:
"""Get the processing fee for this payment method."""
pass
class CreditCardProcessor(PaymentProcessor):
"""Concrete implementation for credit card payments."""
def __init__(self, card_number: str, expiry_date: str):
self.card_number = card_number
self.expiry_date = expiry_date
self._processing_fee = 0.025 # 2.5%
def process_payment(self, amount: float) -> bool:
"""Process credit card payment with validation."""
if amount <= 0:
return False
if not self._validate_card():
return False
# Simulate payment processing
total_amount = amount + (amount * self._processing_fee)
print(f"Processing credit card payment: ${total_amount:.2f}")
return True
def get_processing_fee(self) -> float:
return self._processing_fee
def _validate_card(self) -> bool:
"""Validate credit card details."""
# Check card number length and that it contains only digits
if len(self.card_number) != 16 or not self.card_number.isdigit():
return False
# Check expiry date format (MM/YY or MM/YYYY)
if not self.expiry_date or '/' not in self.expiry_date:
return False
try:
month, year = self.expiry_date.split('/')
month = int(month)
year = int(year)
# Validate month (1-12)
if month < 1 or month > 12:
return False
# Validate year (assuming 2-digit years are 20xx)
if len(str(year)) == 2:
year = 2000 + year
return True
except (ValueError, TypeError):
return False
class BankTransferProcessor(PaymentProcessor):
"""Concrete implementation for bank transfer payments."""
def __init__(self, account_number: str, routing_number: str):
if not account_number or not routing_number:
raise ValueError("Account number and routing number cannot be empty")
if not account_number.isdigit() or not routing_number.isdigit():
raise ValueError("Account number and routing number must contain only digits")
self.account_number = account_number
self.routing_number = routing_number
self._processing_fee = 0.01 # 1%
def process_payment(self, amount: float) -> bool:
"""Process bank transfer payment."""
if amount <= 0:
return False
total_amount = amount + (amount * self._processing_fee)
print(f"Processing bank transfer: ${total_amount:.2f}")
return True
def get_processing_fee(self) -> float:
return self._processing_fee
@dataclass
class OrderItem:
"""Data class for order items."""
product_id: str
name: str
price: float
quantity: int
def __post_init__(self):
"""Validate order item data after initialization."""
if self.price < 0:
raise ValueError("Price cannot be negative")
if self.quantity < 1:
raise ValueError("Quantity must be at least 1")
if not self.product_id or not self.name:
raise ValueError("Product ID and name cannot be empty")
@property
def total_price(self) -> float:
"""Calculate total price for this item."""
return self.price * self.quantity
class Order:
"""Represents a customer order with multiple items."""
def __init__(self, order_id: str, customer_name: str):
if not order_id or not customer_name:
raise ValueError("Order ID and customer name cannot be empty")
self.order_id = order_id
self.customer_name = customer_name
self.items: List[OrderItem] = []
self._status = "pending"
def add_item(self, item: OrderItem) -> None:
"""Add an item to the order."""
self.items.append(item)
def remove_item(self, product_id: str) -> bool:
"""Remove an item from the order."""
for i, item in enumerate(self.items):
if item.product_id == product_id:
del self.items[i]
return True
return False
@property
def total_amount(self) -> float:
"""Calculate total order amount."""
return sum(item.total_price for item in self.items)
@property
def status(self) -> str:
return self._status
def set_status(self, status: str) -> None:
"""Set order status with validation."""
valid_statuses = ["pending", "processing", "shipped", "delivered", "cancelled"]
if status in valid_statuses:
self._status = status
else:
raise ValueError(f"Invalid status: {status}")
class OrderProcessor:
"""Facade pattern implementation for order processing."""
def __init__(self):
self.payment_processors: Dict[PaymentMethod, PaymentProcessor] = {}
def register_payment_processor(self, method: PaymentMethod, processor: PaymentProcessor) -> None:
"""Register a payment processor for a specific method."""
self.payment_processors[method] = processor
def process_order(self, order: Order, payment_method: PaymentMethod) -> bool:
"""Process an order with the specified payment method."""
if not order.items:
raise ValueError("Cannot process order with no items")
if payment_method not in self.payment_processors:
raise ValueError(f"No processor registered for {payment_method.value}")
processor = self.payment_processors[payment_method]
# Process payment
if processor.process_payment(order.total_amount):
order.set_status("processing")
return True
else:
order.set_status("cancelled")
return False
def get_order_summary(self, order: Order) -> Dict[str, Any]:
"""Generate order summary."""
return {
"order_id": order.order_id,
"customer_name": order.customer_name,
"total_amount": order.total_amount,
"item_count": len(order.items),
"status": order.status,
"items": [
{
"product_id": item.product_id,
"name": item.name,
"quantity": item.quantity,
"total_price": item.total_price
}
for item in order.items
]
}
# Singleton pattern implementation
class Logger:
"""Singleton logger for order processing."""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.logs = []
return cls._instance
def log(self, message: str) -> None:
"""Add a log message."""
if not message:
raise ValueError("Log message cannot be empty")
self.logs.append(message)
print(f"[LOG] {message}")
def get_logs(self) -> List[str]:
"""Get all logs."""
return self.logs.copy()
def main():
"""Demonstrate OOP concepts and design patterns."""
try:
logger = Logger()
# Create order
order = Order("ORD-001", "John Doe")
# Add items to order
items = [
OrderItem("PROD-001", "Laptop", 999.99, 1),
OrderItem("PROD-002", "Mouse", 29.99, 2),
OrderItem("PROD-003", "Keyboard", 89.99, 1)
]
for item in items:
order.add_item(item)
# Create payment processors
credit_processor = CreditCardProcessor("1234567890123456", "12/25")
bank_processor = BankTransferProcessor("987654321", "123456789")
# Create order processor
processor = OrderProcessor()
processor.register_payment_processor(PaymentMethod.CREDIT_CARD, credit_processor)
processor.register_payment_processor(PaymentMethod.BANK_TRANSFER, bank_processor)
# Process order with credit card
logger.log("Processing order with credit card...")
success = processor.process_order(order, PaymentMethod.CREDIT_CARD)
if success:
logger.log("Order processed successfully!")
# Generate and display order summary
summary = processor.get_order_summary(order)
print("\n=== Order Summary ===")
print(json.dumps(summary, indent=2))
else:
logger.log("Order processing failed!")
# Demonstrate polymorphism
print("\n=== Payment Processor Comparison ===")
processors = [credit_processor, bank_processor]
for i, proc in enumerate(processors):
fee = proc.get_processing_fee()
print(f"Processor {i+1} fee: {fee:.1%}")
except Exception as e:
print(f"Error in main function: {e}")
if __name__ == "__main__":
main()
Exercise Tips
- Start with simple classes before implementing complex inheritance.
- Use @dataclass decorator for simple data classes.
- Implement __repr__ and __str__ methods for better object representation.
- Practice composition by creating classes that use other classes.