Advanced OOP: Mastering Python Classes
Welcome to Advanced OOP! You’ve learned the fundamentals - now let’s explore advanced techniques that will make you an OOP master. We’ll cover class variables, static methods, method resolution order, and design patterns.
Class Variables vs Instance Variables
Understanding the Difference
class Employee:
# Class variables (shared by all instances)
company_name = "Tech Corp"
total_employees = 0
employee_ids = []
def __init__(self, name, salary):
# Instance variables (unique to each instance)
self.name = name
self.salary = salary
self.id = len(Employee.employee_ids) + 1
# Modify class variables
Employee.total_employees += 1
Employee.employee_ids.append(self.id)
@classmethod
def get_total_employees(cls):
"""Class method - works with class, not instance."""
return cls.total_employees
@staticmethod
def is_valid_salary(salary):
"""Static method - utility function, no self or cls."""
return isinstance(salary, (int, float)) and salary > 0
# Test class vs instance variables
emp1 = Employee("Alice", 50000)
emp2 = Employee("Bob", 60000)
print(f"Total employees: {Employee.get_total_employees()}") # 2
print(f"Alice's company: {emp1.company_name}") # "Tech Corp"
print(f"Bob's company: {emp2.company_name}") # "Tech Corp"
# Changing class variable affects all instances
Employee.company_name = "Super Tech Corp"
print(f"Alice's company: {emp1.company_name}") # "Super Tech Corp"
# Instance variables are independent
emp1.name = "Alice Johnson"
print(f"emp1 name: {emp1.name}") # "Alice Johnson"
print(f"emp2 name: {emp2.name}") # "Bob"
# Static method usage
print(f"Valid salary 50000: {Employee.is_valid_salary(50000)}") # True
print(f"Valid salary -100: {Employee.is_valid_salary(-100)}") # False
Class Methods and Static Methods
When to Use Each
class DateUtils:
"""Utility class for date operations."""
def __init__(self, date_string):
self.date_string = date_string
@classmethod
def from_timestamp(cls, timestamp):
"""Create instance from Unix timestamp."""
import datetime
dt = datetime.datetime.fromtimestamp(timestamp)
date_string = dt.strftime("%Y-%m-%d")
return cls(date_string)
@classmethod
def today(cls):
"""Create instance for today's date."""
import datetime
today = datetime.datetime.now().strftime("%Y-%m-%d")
return cls(today)
@staticmethod
def is_valid_date(date_string):
"""Check if date string is valid (static method)."""
import re
pattern = r'^\d{4}-\d{2}-\d{2}$'
if not re.match(pattern, date_string):
return False
# Additional validation could go here
return True
@staticmethod
def days_between(date1, date2):
"""Calculate days between two dates (static method)."""
import datetime
d1 = datetime.datetime.strptime(date1, "%Y-%m-%d")
d2 = datetime.datetime.strptime(date2, "%Y-%m-%d")
return abs((d2 - d1).days)
# Test class and static methods
# Class methods
today_date = DateUtils.today()
timestamp_date = DateUtils.from_timestamp(1609459200) # 2021-01-01
print(f"Today's date: {today_date.date_string}")
print(f"From timestamp: {timestamp_date.date_string}")
# Static methods
print(f"Is '2021-01-01' valid? {DateUtils.is_valid_date('2021-01-01')}")
print(f"Is 'invalid' valid? {DateUtils.is_valid_date('invalid')}")
print(f"Days between: {DateUtils.days_between('2021-01-01', '2021-01-05')}")
Method Resolution Order (MRO)
Understanding Multiple Inheritance
class A:
def method(self):
return "Method from A"
class B(A):
def method(self):
return "Method from B"
class C(A):
def method(self):
return "Method from C"
class D(B, C):
pass
# Method Resolution Order
print(f"MRO for D: {D.__mro__}")
# Output: (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
d = D()
print(f"d.method(): {d.method()}") # "Method from B" (B comes first)
# Check MRO manually
print(f"D.__bases__: {D.__bases__}") # (<class '__main__.B'>, <class '__main__.C'>)
Complex Multiple Inheritance
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "Some animal sound"
class Walker:
def move(self):
return "Walking"
class Swimmer:
def move(self):
return "Swimming"
class Flyer:
def move(self):
return "Flying"
class Duck(Animal, Walker, Swimmer, Flyer):
def speak(self):
return "Quack!"
# Test multiple inheritance
duck = Duck("Donald")
print(f"Name: {duck.name}")
print(f"Speak: {duck.speak()}")
# Method Resolution Order determines which move() is used
print(f"Move: {duck.move()}") # "Walking" (Walker comes first after Animal)
# Can access all methods through class
print(f"Walk: {Walker.move(duck)}")
print(f"Swim: {Swimmer.move(duck)}")
print(f"Fly: {Flyer.move(duck)}")
print(f"MRO: {Duck.__mro__}")
Composition vs Inheritance
When to Use Composition
# Inheritance approach (not always best)
class Engine:
def start(self):
return "Engine started"
class Car(Engine): # Car IS-A Engine?
def __init__(self, make, model):
self.make = make
self.model = model
def drive(self):
engine_status = self.start() # Inheriting start() method
return f"{self.make} {self.model} is driving. {engine_status}"
# Composition approach (better)
class Engine:
def start(self):
return "Engine started"
def stop(self):
return "Engine stopped"
class Car:
def __init__(self, make, model):
self.make = make
self.model = model
self.engine = Engine() # Car HAS-A Engine
def drive(self):
engine_status = self.engine.start()
return f"{self.make} {self.model} is driving. {engine_status}"
def park(self):
engine_status = self.engine.stop()
return f"{self.make} {self.model} is parked. {engine_status}"
# Test composition
car = Car("Toyota", "Camry")
print(car.drive())
print(car.park())
Composition Example: Computer System
class CPU:
def __init__(self, cores, speed):
self.cores = cores
self.speed = speed
def process(self, task):
return f"CPU ({self.cores} cores @ {self.speed}GHz) processing: {task}"
class RAM:
def __init__(self, size_gb):
self.size_gb = size_gb
def allocate(self, amount):
if amount > self.size_gb:
raise MemoryError("Not enough RAM")
return f"Allocated {amount}GB RAM"
class Storage:
def __init__(self, type_, capacity_gb):
self.type = type_
self.capacity_gb = capacity_gb
def read(self, file_path):
return f"Reading {file_path} from {self.type} storage"
def write(self, file_path, data):
return f"Writing {len(data)} bytes to {file_path} on {self.type} storage"
class Computer:
def __init__(self, cpu_cores, cpu_speed, ram_gb, storage_type, storage_gb):
self.cpu = CPU(cpu_cores, cpu_speed)
self.ram = RAM(ram_gb)
self.storage = Storage(storage_type, storage_gb)
def run_program(self, program_name):
# Use composition to delegate tasks
cpu_result = self.cpu.process(f"Running {program_name}")
ram_result = self.ram.allocate(2) # Assume program needs 2GB
storage_result = self.storage.read(f"{program_name}.exe")
return f"{cpu_result}\n{ram_result}\n{storage_result}"
# Test composition
computer = Computer(cpu_cores=8, cpu_speed=3.5, ram_gb=16,
storage_type="SSD", storage_gb=512)
print(computer.run_program("Python"))
Abstract Base Classes (ABC)
Enforcing Interfaces
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
def __init__(self, amount):
self.amount = amount
@abstractmethod
def process_payment(self):
"""Must be implemented by subclasses."""
pass
@abstractmethod
def validate(self):
"""Must be implemented by subclasses."""
pass
def get_receipt(self):
"""Concrete method that can be inherited."""
return f"Payment of ${self.amount} processed successfully"
class CreditCardProcessor(PaymentProcessor):
def __init__(self, amount, card_number):
super().__init__(amount)
self.card_number = card_number
def validate(self):
if len(self.card_number.replace(" ", "")) < 13:
raise ValueError("Invalid card number")
return True
def process_payment(self):
self.validate()
last_four = self.card_number[-4:]
return f"Charged ${self.amount} to card ending in {last_four}"
class PayPalProcessor(PaymentProcessor):
def __init__(self, amount, email):
super().__init__(amount)
self.email = email
def validate(self):
if "@" not in self.email:
raise ValueError("Invalid PayPal email")
return True
def process_payment(self):
self.validate()
return f"Charged ${self.amount} via PayPal account {self.email}"
# Test ABC
processors = [
CreditCardProcessor(100, "4111111111111111"),
PayPalProcessor(50, "user@example.com")
]
for processor in processors:
try:
result = processor.process_payment()
receipt = processor.get_receipt()
print(f"{result}\n{receipt}\n")
except ValueError as e:
print(f"Validation error: {e}\n")
# This would fail - can't instantiate abstract class
# processor = PaymentProcessor(100) # TypeError
Data Classes
Simplified Class Creation
from dataclasses import dataclass, field
from typing import List
@dataclass
class Person:
name: str
age: int
email: str = "" # Default value
def greet(self):
return f"Hello, I'm {self.name}"
@dataclass
class Address:
street: str
city: str
zip_code: str
@dataclass
class Employee:
name: str
age: int
salary: float
address: Address
skills: List[str] = field(default_factory=list) # Mutable default
def give_raise(self, percentage):
self.salary *= (1 + percentage / 100)
def add_skill(self, skill):
if skill not in self.skills:
self.skills.append(skill)
# Test data classes
person = Person("Alice", 30, "alice@example.com")
print(person) # Person(name='Alice', age=30, email='alice@example.com')
print(person.greet())
address = Address("123 Main St", "Anytown", "12345")
employee = Employee("Bob", 25, 50000, address, ["Python", "Java"])
print(f"\nEmployee: {employee}")
employee.give_raise(10)
employee.add_skill("JavaScript")
print(f"After raise and skill: {employee}")
Properties with Advanced Features
Advanced Property Usage
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 # Uses celsius setter for validation
@property
def kelvin(self):
return self._celsius + 273.15
@kelvin.setter
def kelvin(self, value):
self.celsius = value - 273.15
# Test advanced properties
temp = Temperature(20)
print(f"Celsius: {temp.celsius}")
print(f"Fahrenheit: {temp.fahrenheit}")
print(f"Kelvin: {temp.kelvin}")
temp.fahrenheit = 100
print(f"After setting Fahrenheit to 100: {temp.celsius}°C")
try:
temp.kelvin = -100 # Below absolute zero
except ValueError as e:
print(f"Error: {e}")
Metaclasses
Classes That Create Classes
class SingletonMeta(type):
"""Metaclass that creates singleton classes."""
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class DatabaseConnection(metaclass=SingletonMeta):
def __init__(self, host, port):
self.host = host
self.port = port
print(f"Connecting to {host}:{port}")
# Test singleton
db1 = DatabaseConnection("localhost", 5432)
db2 = DatabaseConnection("localhost", 5432) # Same instance
print(f"db1 is db2: {db1 is db2}") # True
print(f"Same host: {db1.host == db2.host}") # True
Practical Examples
Example 1: Plugin System
from abc import ABC, abstractmethod
import importlib
import os
class Plugin(ABC):
@abstractmethod
def execute(self, data):
pass
@property
@abstractmethod
def name(self):
pass
class UppercasePlugin(Plugin):
name = "uppercase"
def execute(self, data):
return data.upper()
class ReversePlugin(Plugin):
name = "reverse"
def execute(self, data):
return data[::-1]
class PluginManager:
def __init__(self):
self.plugins = {}
def register_plugin(self, plugin_class):
if not issubclass(plugin_class, Plugin):
raise TypeError("Plugin must inherit from Plugin class")
plugin_instance = plugin_class()
self.plugins[plugin_instance.name] = plugin_instance
def execute_plugin(self, plugin_name, data):
if plugin_name not in self.plugins:
raise ValueError(f"Plugin '{plugin_name}' not found")
return self.plugins[plugin_name].execute(data)
def list_plugins(self):
return list(self.plugins.keys())
# Test plugin system
manager = PluginManager()
manager.register_plugin(UppercasePlugin)
manager.register_plugin(ReversePlugin)
print(f"Available plugins: {manager.list_plugins()}")
text = "Hello World"
print(f"Original: {text}")
print(f"Uppercase: {manager.execute_plugin('uppercase', text)}")
print(f"Reverse: {manager.execute_plugin('reverse', text)}")
Example 2: Observer Pattern
from typing import List, Callable
class Subject:
"""Observable subject."""
def __init__(self):
self._observers: List[Callable] = []
def attach(self, observer: Callable):
"""Attach an observer."""
self._observers.append(observer)
def detach(self, observer: Callable):
"""Detach an observer."""
self._observers.remove(observer)
def notify(self, event_data=None):
"""Notify all observers."""
for observer in self._observers:
observer(event_data)
class NewsPublisher(Subject):
def __init__(self):
super().__init__()
self._news = []
def publish_news(self, news_item):
self._news.append(news_item)
print(f"Publishing news: {news_item}")
self.notify(news_item)
class EmailSubscriber:
def __init__(self, name, email):
self.name = name
self.email = email
def update(self, news):
print(f"Email to {self.email}: New news - {news}")
class SMSSubscriber:
def __init__(self, name, phone):
self.name = name
self.phone = phone
def update(self, news):
print(f"SMS to {self.phone}: {news[:50]}...")
# Test observer pattern
publisher = NewsPublisher()
# Create subscribers
email_sub = EmailSubscriber("Alice", "alice@example.com")
sms_sub = SMSSubscriber("Bob", "555-0123")
# Attach observers
publisher.attach(email_sub.update)
publisher.attach(sms_sub.update)
# Publish news
publisher.publish_news("Breaking: Python 4.0 released!")
publisher.publish_news("Tech stocks surge after AI breakthrough")
# Detach one observer
publisher.detach(sms_sub.update)
publisher.publish_news("Weather: Sunny skies expected")
Example 3: Factory Pattern
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def speak(self):
pass
@abstractmethod
def move(self):
pass
class Dog(Animal):
def speak(self):
return "Woof!"
def move(self):
return "Runs on four legs"
class Cat(Animal):
def speak(self):
return "Meow!"
def move(self):
return "Walks gracefully"
class Bird(Animal):
def speak(self):
return "Tweet!"
def move(self):
return "Flies through the air"
class AnimalFactory:
"""Factory for creating animals."""
_animal_classes = {
"dog": Dog,
"cat": Cat,
"bird": Bird
}
@classmethod
def create_animal(cls, animal_type, **kwargs):
"""Create an animal of the specified type."""
animal_type = animal_type.lower()
if animal_type not in cls._animal_classes:
raise ValueError(f"Unknown animal type: {animal_type}")
animal_class = cls._animal_classes[animal_type]
return animal_class(**kwargs)
@classmethod
def register_animal(cls, animal_type, animal_class):
"""Register a new animal type."""
if not issubclass(animal_class, Animal):
raise TypeError("Animal class must inherit from Animal")
cls._animal_classes[animal_type.lower()] = animal_class
@classmethod
def get_available_types(cls):
"""Get list of available animal types."""
return list(cls._animal_classes.keys())
# Test factory pattern
animals = []
for animal_type in ["dog", "cat", "bird"]:
animal = AnimalFactory.create_animal(animal_type)
animals.append(animal)
print(f"{animal_type.capitalize()}: {animal.speak()}, {animal.move()}")
print(f"\nAvailable types: {AnimalFactory.get_available_types()}")
# Register a new animal type
class Fish(Animal):
def speak(self):
return "Blub!"
def move(self):
return "Swims in water"
AnimalFactory.register_animal("fish", Fish)
fish = AnimalFactory.create_animal("fish")
print(f"Fish: {fish.speak()}, {fish.move()}")
Practice Exercises
Exercise 1: Configuration Manager
Create a ConfigManager class with:
- Class methods for loading/saving configuration
- Instance methods for getting/setting values
- Validation of configuration values
- Singleton pattern to ensure only one instance
Exercise 2: Event System
Build an event system with:
Eventbase class with subclasses for different event typesEventDispatcherclass for managing subscribers- Decorator for marking event handler methods
- Support for both sync and async event handlers
Exercise 3: Database ORM
Create a simple ORM system with:
Modelbase class with metaclass for field management- Field classes (
CharField,IntegerField, etc.) - Query methods (
save(),delete(),find()) - Relationship support (ForeignKey)
Exercise 4: Plugin Architecture
Build a plugin architecture with:
- Abstract
Pluginbase class PluginManagerfor loading/unloading plugins- Hook system for extending functionality
- Dependency management between plugins
Exercise 5: State Machine
Create a state machine framework with:
StateMachineclass with state transitionsStatebase class for implementing state behavior- Guard conditions for transitions
- Entry/exit actions for states
Summary
Advanced OOP techniques take your classes to the next level:
Class vs Instance Variables:
class MyClass:
class_var = "shared" # Class variable
def __init__(self):
self.instance_var = "unique" # Instance variable
Class Methods & Static Methods:
@classmethod
def class_method(cls): pass
@staticmethod
def static_method(): pass
Method Resolution Order:
class D(B, C): pass
print(D.__mro__) # Shows inheritance order
Composition over Inheritance:
class Car:
def __init__(self):
self.engine = Engine() # HAS-A relationship
Abstract Base Classes:
from abc import ABC, abstractmethod
class MyABC(ABC):
@abstractmethod
def required_method(self): pass
Data Classes:
@dataclass
class Person:
name: str
age: int
Key Advanced Concepts:
- Class variables for shared state
- Class/static methods for utility functions
- MRO for multiple inheritance resolution
- Composition for flexible object relationships
- ABCs for interface contracts
- Data classes for simple data containers
- Metaclasses for class customization
Design Patterns:
- Singleton (one instance)
- Factory (object creation)
- Observer (event handling)
- Strategy (algorithm selection)
Next: Modules and Packages - organizing your code! 📦