Design Patterns in Python
Difficulty: Medium-Hard | Companies: Google, Meta, Amazon, Netflix, Stripe
Creational Patterns
Singleton Pattern
from typing import Any, Optional
import threading
class SingletonMeta(type):
"""Thread-safe Singleton metaclass."""
_instances = {}
_lock = threading.Lock()
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
with cls._lock:
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class Database(metaclass=SingletonMeta):
"""Singleton database connection."""
def __init__(self):
self.connection = None
self._connect()
def _connect(self):
print("Establishing database connection...")
self.connection = "Connected"
def query(self, sql: str):
return f"Executing: {sql}"
# Alternative: Decorator-based Singleton
def singleton(cls):
"""Singleton decorator."""
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class Logger:
"""Singleton logger."""
def __init__(self):
self.logs = []
def log(self, message: str):
self.logs.append(message)
print(f"LOG: {message}")
# Usage
db1 = Database()
db2 = Database()
print(db1 is db2) # True
logger1 = Logger()
logger2 = Logger()
print(logger1 is logger2) # True
âšī¸
Factory Pattern
from abc import ABC, abstractmethod
from typing import Dict, Type, Any
class Notification(ABC):
"""Abstract notification class."""
@abstractmethod
def send(self, message: str) -> bool:
pass
@abstractmethod
def get_type(self) -> str:
pass
class EmailNotification(Notification):
def __init__(self, smtp_server: str, port: int):
self.smtp_server = smtp_server
self.port = port
def send(self, message: str) -> bool:
print(f"Email via {self.smtp_server}:{self.port}: {message}")
return True
def get_type(self) -> str:
return "email"
class SMSNotification(Notification):
def __init__(self, api_key: str):
self.api_key = api_key
def send(self, message: str) -> bool:
print(f"SMS via API: {message}")
return True
def get_type(self) -> str:
return "sms"
class PushNotification(Notification):
def __init__(self, device_token: str):
self.device_token = device_token
def send(self, message: str) -> bool:
print(f"Push to {self.device_token}: {message}")
return True
def get_type(self) -> str:
return "push"
class NotificationFactory:
"""Factory for creating notifications."""
_registry: Dict[str, Type[Notification]] = {}
@classmethod
def register(cls, notification_type: str, notification_class: Type[Notification]):
cls._registry[notification_type] = notification_class
@classmethod
def create(cls, notification_type: str, **kwargs) -> Notification:
if notification_type not in cls._registry:
raise ValueError(f"Unknown notification type: {notification_type}")
return cls._registry[notification_type](**kwargs)
@classmethod
def list_types(cls):
return list(cls._registry.keys())
# Register notification types
NotificationFactory.register("email", EmailNotification)
NotificationFactory.register("sms", SMSNotification)
NotificationFactory.register("push", PushNotification)
# Usage
email = NotificationFactory.create("email", smtp_server="smtp.gmail.com", port=587)
email.send("Hello via email!")
Structural Patterns
Adapter Pattern
from typing import Any, Dict
import json
class EuropeanSocket:
"""European power socket."""
def voltage(self) -> int:
return 230
def live(self) -> int:
return 1
def neutral(self) -> int:
return -1
def ground(self) -> int:
return 0
class AmericanSocket:
"""American power socket."""
def voltage(self) -> int:
return 120
def live(self) -> int:
return 1
def neutral(self) -> int:
return -1
class Adapter:
"""Adapter to make European device work with American socket."""
def __init__(self, socket: AmericanSocket):
self.socket = socket
def voltage(self) -> int:
return self.socket.voltage()
def live(self) -> int:
return self.socket.live()
def neutral(self) -> int:
return self.socket.neutral()
def ground(self) -> int:
return 0 # Adapter provides ground
class EuropeanDevice:
"""Device requiring European voltage."""
def __init__(self, socket):
self.socket = socket
def power_on(self):
if self.socket.voltage() > 200:
print("Device powered on safely")
else:
print("Voltage too low!")
# Usage
american_socket = AmericanSocket()
adapter = Adapter(american_socket)
device = EuropeanDevice(adapter)
device.power_on() # Works with adapter
# Data format adapter
class XMLData:
"""Simulated XML data source."""
def __init__(self, data: Dict):
self.data = data
def to_xml(self) -> str:
xml = "<data>"
for key, value in self.data.items():
xml += f"<{key}>{value}</{key}>"
xml += "</data>"
return xml
class JSONAdapter:
"""Adapter to convert XML interface to JSON."""
def __init__(self, xml_data: XMLData):
self.xml_data = xml_data
def to_json(self) -> str:
# Convert XML to JSON
xml_str = self.xml_data.to_xml()
# Simple conversion (in real code, use proper XML parser)
return json.dumps(self.xml_data.data)
# Usage
xml_data = XMLData({"name": "John", "age": 30})
json_adapter = JSONAdapter(xml_data)
print(json_adapter.to_json())
â ī¸
Behavioral Patterns
Strategy Pattern
from abc import ABC, abstractmethod
from typing import List, Callable
from functools import reduce
class SortStrategy(ABC):
"""Abstract sorting strategy."""
@abstractmethod
def sort(self, data: List) -> List:
pass
@abstractmethod
def get_name(self) -> str:
pass
class BubbleSort(SortStrategy):
def sort(self, data: List) -> List:
arr = data.copy()
n = len(arr)
for i in range(n):
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
return arr
def get_name(self) -> str:
return "Bubble Sort"
class QuickSort(SortStrategy):
def sort(self, data: List) -> List:
if len(data) <= 1:
return data
pivot = data[len(data) // 2]
left = [x for x in data if x < pivot]
middle = [x for x in data if x == pivot]
right = [x for x in data if x > pivot]
return self.sort(left) + middle + self.sort(right)
def get_name(self) -> str:
return "Quick Sort"
class MergeSort(SortStrategy):
def sort(self, data: List) -> List:
if len(data) <= 1:
return data
mid = len(data) // 2
left = self.sort(data[:mid])
right = self.sort(data[mid:])
return self._merge(left, right)
def _merge(self, left: List, right: List) -> List:
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
def get_name(self) -> str:
return "Merge Sort"
class Sorter:
"""Context class for sorting strategies."""
def __init__(self, strategy: SortStrategy):
self._strategy = strategy
def set_strategy(self, strategy: SortStrategy):
self._strategy = strategy
def sort(self, data: List) -> List:
print(f"Sorting with {self._strategy.get_name()}")
return self._strategy.sort(data)
# Usage
data = [64, 34, 25, 12, 22, 11, 90]
sorter = Sorter(QuickSort())
print(sorter.sort(data))
sorter.set_strategy(MergeSort())
print(sorter.sort(data))
Command Pattern
from abc import ABC, abstractmethod
from typing import List, Any
from datetime import datetime
class Command(ABC):
"""Abstract command interface."""
@abstractmethod
def execute(self) -> Any:
pass
@abstractmethod
def undo(self) -> Any:
pass
@abstractmethod
def description(self) -> str:
pass
class TextDocument:
"""Text document receiver."""
def __init__(self):
self.content = ""
self.cursor_position = 0
def insert(self, text: str, position: int):
self.content = self.content[:position] + text + self.content[position:]
self.cursor_position = position + len(text)
def delete(self, position: int, length: int) -> str:
deleted = self.content[position:position + length]
self.content = self.content[:position] + self.content[position + length:]
self.cursor_position = position
return deleted
def replace(self, position: int, length: int, new_text: str) -> str:
deleted = self.content[position:position + length]
self.content = self.content[:position] + new_text + self.content[position + length:]
self.cursor_position = position + len(new_text)
return deleted
class InsertTextCommand(Command):
"""Insert text command."""
def __init__(self, document: TextDocument, text: str, position: int):
self.document = document
self.text = text
self.position = position
self.timestamp = datetime.now()
def execute(self):
self.document.insert(self.text, self.position)
def undo(self):
self.document.delete(self.position, len(self.text))
def description(self):
return f"Insert '{self.text}' at position {self.position}"
class DeleteTextCommand(Command):
"""Delete text command."""
def __init__(self, document: TextDocument, position: int, length: int):
self.document = document
self.position = position
self.length = length
self.deleted_text = ""
self.timestamp = datetime.now()
def execute(self):
self.deleted_text = self.document.delete(self.position, self.length)
def undo(self):
self.document.insert(self.deleted_text, self.position)
def description(self):
return f"Delete {self.length} chars at position {self.position}"
class CommandHistory:
"""Manages command history with undo/redo."""
def __init__(self):
self._undo_stack: List[Command] = []
self._redo_stack: List[Command] = []
def execute(self, command: Command):
command.execute()
self._undo_stack.append(command)
self._redo_stack.clear()
print(f"Executed: {command.description()}")
def undo(self) -> bool:
if not self._undo_stack:
return False
command = self._undo_stack.pop()
command.undo()
self._redo_stack.append(command)
print(f"Undone: {command.description()}")
return True
def redo(self) -> bool:
if not self._redo_stack:
return False
command = self._redo_stack.pop()
command.execute()
self._undo_stack.append(command)
print(f"Redone: {command.description()}")
return True
def get_history(self) -> List[str]:
return [cmd.description() for cmd in self._undo_stack]
# Usage
doc = TextDocument()
history = CommandHistory()
history.execute(InsertTextCommand(doc, "Hello", 0))
history.execute(InsertTextCommand(doc, " World", 5))
print(doc.content) # Hello World
history.undo()
print(doc.content) # Hello
history.redo()
print(doc.content) # Hello World
âšī¸
Observer Pattern
from typing import Callable, Dict, List, Any
from dataclasses import dataclass
from datetime import datetime
import weakref
@dataclass
class Event:
"""Event data class."""
name: str
data: Any
timestamp: datetime
source: str
class EventEmitter:
"""Event emitter with weak references."""
def __init__(self):
self._listeners: Dict[str, List] = {}
def on(self, event: str, callback: Callable) -> Callable:
"""Register event listener."""
if event not in self._listeners:
self._listeners[event] = []
# Use weak reference to prevent memory leaks
ref = weakref.WeakMethod(callback, self._on_ref_deleted(event))
self._listeners[event].append(ref)
def unsubscribe():
if event in self._listeners:
self._listeners[event] = [
r for r in self._listeners[event]
if r() is not None and r() != callback
]
return unsubscribe
def _on_ref_deleted(self, event: str) -> Callable:
def callback(ref):
if event in self._listeners:
self._listeners[event] = [
r for r in self._listeners[event] if r is not ref
]
return callback
def emit(self, event: str, data: Any = None, source: str = ""):
"""Emit event to all listeners."""
if event not in self._listeners:
return
event_data = Event(
name=event,
data=data,
timestamp=datetime.now(),
source=source
)
dead_refs = []
for ref in self._listeners[event]:
callback = ref()
if callback is not None:
callback(event_data)
else:
dead_refs.append(ref)
# Cleanup dead references
for ref in dead_refs:
self._listeners[event].remove(ref)
def once(self, event: str, callback: Callable) -> Callable:
"""Register one-time listener."""
def wrapper(event_data):
callback(event_data)
unsubscribe()
unsubscribe = self.on(event, wrapper)
return unsubscribe
# Usage
emitter = EventEmitter()
def on_user_created(event: Event):
print(f"User created: {event.data}")
def send_welcome_email(event: Event):
print(f"Sending email to {event.data.get('email')}")
emitter.on("user_created", on_user_created)
emitter.on("user_created", send_welcome_email)
emitter.emit("user_created", {"name": "John", "email": "[email protected]"})
Follow-Up Questions
-
When would you use the Factory pattern over direct object instantiation?
-
How do you implement thread-safe Singleton in Python?
-
Explain the difference between Observer and Pub-Sub patterns.
-
When is the Strategy pattern more appropriate than inheritance?
-
How do you handle command serialization in the Command pattern?