🐍 Design Patterns In Python Secrets That Will Make You!
Hey there! Ready to dive into Design Patterns In Python? This friendly guide will walk you through everything step-by-step with easy-to-follow examples. Perfect for beginners and pros alike!
🚀
💡 Pro tip: This is one of those techniques that will make you look like a data science wizard! Singleton Pattern The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. - Made Simple!
Ready for some cool stuff? Here’s how we can tackle this:
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super(Singleton, cls).__new__(cls)
return cls._instance
# Usage
s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # True
🚀
🎉 You’re doing great! This concept might seem tricky at first, but you’ve got this! Factory Pattern The Factory pattern provides an interface for creating objects in a super-class, but allows subclasses to alter the type of objects that will be created. - Made Simple!
Let’s break this down together! Here’s how we can tackle this:
class Animal:
def __init__(self, species):
self.species = species
def show(self):
print(f"I'm a {self.species}")
class AnimalFactory:
def create_animal(self, species):
return Animal(species)
# Usage
factory = AnimalFactory()
dog = factory.create_animal("Dog")
cat = factory.create_animal("Cat")
dog.show() # I'm a Dog
cat.show() # I'm a Cat
🚀
✨ Cool fact: Many professional data scientists use this exact approach in their daily work! Observer Pattern The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. - Made Simple!
Here’s a handy trick you’ll love! Here’s how we can tackle this:
class Subject:
def __init__(self):
self.observers = []
def attach(self, observer):
self.observers.append(observer)
def detach(self, observer):
self.observers.remove(observer)
def notify(self, data):
for observer in self.observers:
observer.update(data)
class Observer:
def update(self, data):
pass
class ConcreteObserver(Observer):
def update(self, data):
print(f"Received data: {data}")
# Usage
subject = Subject()
observer1 = ConcreteObserver()
observer2 = ConcreteObserver()
subject.attach(observer1)
subject.attach(observer2)
subject.notify("Hello, World!")
🚀
🔥 Level up: Once you master this, you’ll be solving problems like a pro! Decorator Pattern The Decorator pattern allows behavior to be added to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class. - Made Simple!
Here’s a handy trick you’ll love! Here’s how we can tackle this:
class Component:
def operation(self):
pass
class ConcreteComponent(Component):
def operation(self):
print("ConcreteComponent.operation()")
class Decorator(Component):
def __init__(self, component):
self.component = component
def operation(self):
self.component.operation()
class ConcreteDecoratorA(Decorator):
def operation(self):
print("ConcreteDecoratorA.operation()")
self.component.operation()
class ConcreteDecoratorB(Decorator):
def operation(self):
print("ConcreteDecoratorB.operation()")
self.component.operation()
# Usage
component = ConcreteComponent()
decorator_a = ConcreteDecoratorA(component)
decorator_b = ConcreteDecoratorB(decorator_a)
decorator_b.operation()
🚀 Strategy Pattern The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients that use it. - Made Simple!
Don’t worry, this is easier than it looks! Here’s how we can tackle this:
class Strategy:
def execute(self, data):
pass
class ConcreteStrategyA(Strategy):
def execute(self, data):
print(f"Executing strategy A with data: {data}")
class ConcreteStrategyB(Strategy):
def execute(self, data):
print(f"Executing strategy B with data: {data}")
class Context:
def __init__(self, strategy):
self.strategy = strategy
def set_strategy(self, strategy):
self.strategy = strategy
def execute(self, data):
self.strategy.execute(data)
# Usage
strategy_a = ConcreteStrategyA()
strategy_b = ConcreteStrategyB()
context = Context(strategy_a)
context.execute("Hello") # Executing strategy A with data: Hello
context.set_strategy(strategy_b)
context.execute("World") # Executing strategy B with data: World
🚀 Adapter Pattern The Adapter pattern allows objects with incompatible interfaces to collaborate by wrapping its own interface around that of an already existing class. - Made Simple!
Don’t worry, this is easier than it looks! Here’s how we can tackle this:
class Target:
def request(self):
print("Target.request()")
class Adaptee:
def specific_request(self):
print("Adaptee.specific_request()")
class Adapter(Target):
def __init__(self, adaptee):
self.adaptee = adaptee
def request(self):
self.adaptee.specific_request()
# Usage
adaptee = Adaptee()
adapter = Adapter(adaptee)
adapter.request() # Adaptee.specific_request()
🚀 Facade Pattern The Facade pattern provides a unified interface to a set of interfaces in a subsystem. It defines a higher-level interface that makes the subsystem easier to use. - Made Simple!
Here’s where it gets exciting! Here’s how we can tackle this:
class SubSystemA:
def operation_a(self):
print("SubSystemA.operation_a()")
class SubSystemB:
def operation_b(self):
print("SubSystemB.operation_b()")
class Facade:
def __init__(self):
self.subsystem_a = SubSystemA()
self.subsystem_b = SubSystemB()
def operation(self):
self.subsystem_a.operation_a()
self.subsystem_b.operation_b()
# Usage
facade = Facade()
facade.operation()
🚀 Proxy Pattern The Proxy pattern provides a surrogate or placeholder for another object to control access to it. - Made Simple!
Let me walk you through this step by step! Here’s how we can tackle this:
class Subject:
def request(self):
print("Subject.request()")
class Proxy:
def __init__(self, subject):
self.subject = subject
def request(self):
if self.check_access():
self.subject.request()
self.log_access()
def check_access(self):
print("Proxy.check_access()")
return True
def log_access(self):
print("Proxy.log_access()")
# Usage
subject = Subject()
proxy = Proxy(subject)
proxy.request()
🚀 Composite Pattern The Composite pattern allows you to compose objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly. - Made Simple!
This next part is really neat! Here’s how we can tackle this:
class Component:
def operation(self):
pass
class Leaf(Component):
def __init__(self, name):
self.name = name
def operation(self):
print(f"Leaf: {self.name}")
class Composite(Component):
def __init__(self):
self.children = []
def add(self, component):
self.children.append(component)
def remove(self, component):
self.children.remove(component)
def operation(self):
for child in self.children:
child.operation()
# Usage
leaf_a = Leaf("A")
leaf_b = Leaf("B")
composite = Composite()
composite.add(leaf_a)
composite.add(leaf_b)
composite.operation()
🚀 Iterator Pattern The Iterator pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation. - Made Simple!
Let me walk you through this step by step! Here’s how we can tackle this:
class Iterator:
def has_next(self):
pass
def next(self):
pass
class ConcreteIterator(Iterator):
def __init__(self, collection):
self.collection = collection
self.index = 0
def has_next(self):
return self.index < len(self.collection)
def next(self):
item = self.collection[self.index]
self.index += 1
return item
class Aggregate:
def __init__(self):
self.items = []
def add(self, item):
self.items.append(item)
def iterator(self):
return ConcreteIterator(self.items)
# Usage
aggregate = Aggregate()
aggregate.add("Item 1")
aggregate.add("Item 2")
aggregate.add("Item 3")
iterator = aggregate.iterator()
while iterator.has_next():
item = iterator.next()
print(item)
🚀 Builder Pattern The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create various representations. - Made Simple!
Here’s a handy trick you’ll love! Here’s how we can tackle this:
class Product:
def __init__(self):
self.parts = []
def add(self, part):
self.parts.append(part)
def display(self):
print("\n".join(self.parts))
class Builder:
def build_part_a(self):
pass
def build_part_b(self):
pass
def get_result(self):
pass
class ConcreteBuilder(Builder):
def __init__(self):
self.product = Product()
def build_part_a(self):
self.product.add("Part A")
def build_part_b(self):
self.product.add("Part B")
def get_result(self):
return self.product
class Director:
def __init__(self):
self.builder = None
def set_builder(self, builder):
self.builder = builder
def construct(self):
self.builder.build_part_a()
self.builder.build_part_b()
# Usage
director = Director()
builder = ConcreteBuilder()
director.set_builder(builder)
director.construct()
product = builder.get_result()
product.display()
🚀 Flyweight Pattern The Flyweight pattern aims to minimize memory usage by sharing data with similar characteristics. - Made Simple!
Ready for some cool stuff? Here’s how we can tackle this:
class Flyweight:
def __init__(self, data):
self.data = data
def operation(self, extrinsic_data):
print(f"Flyweight ({self.data}): {extrinsic_data}")
class FlyweightFactory:
def __init__(self):
self.flyweights = {}
def get_flyweight(self, key):
if key not in self.flyweights:
self.flyweights[key] = Flyweight(key)
return self.flyweights[key]
# Usage
factory = FlyweightFactory()
flyweight_a = factory.get_flyweight("A")
flyweight_a.operation("Extrinsic Data 1")
flyweight_b = factory.get_flyweight("B")
flyweight_b.operation("Extrinsic Data 2")
flyweight_a.operation("Extrinsic Data 3")
🚀 Chain of Responsibility Pattern The Chain of Responsibility pattern allows an event to be passed along a chain of objects, with each object having a chance to handle the event. - Made Simple!
Let’s break this down together! Here’s how we can tackle this:
class Handler:
def __init__(self, successor=None):
self.successor = successor
def handle(self, request):
pass
class ConcreteHandler1(Handler):
def handle(self, request):
if request >= 0 and request < 10:
print(f"ConcreteHandler1 handled request: {request}")
elif self.successor:
self.successor.handle(request)
class ConcreteHandler2(Handler):
def handle(self, request):
if request >= 10 and request < 20:
print(f"ConcreteHandler2 handled request: {request}")
elif self.successor:
self.successor.handle(request)
class ConcreteHandler3(Handler):
def handle(self, request):
if request >= 20 and request < 30:
print(f"ConcreteHandler3 handled request: {request}")
else:
print(f"Request {request} was not handled")
# Usage
handler1 = ConcreteHandler1()
handler2 = ConcreteHandler2(handler1)
handler3 = ConcreteHandler3(handler2)
handler3.handle(5) # ConcreteHandler1 handled request: 5
handler3.handle(15) # ConcreteHandler2 handled request: 15
handler3.handle(25) # ConcreteHandler3 handled request: 25
handler3.handle(35) # Request 35 was not handled
🚀 Command Pattern The Command pattern encapsulates a request as an object, thereby allowing for the parameterization of clients with different requests, queue or log requests, and support undoable operations. - Made Simple!
Don’t worry, this is easier than it looks! Here’s how we can tackle this:
class Command:
def execute(self):
pass
class ConcreteCommand(Command):
def __init__(self, receiver):
self.receiver = receiver
def execute(self):
self.receiver.action()
class Receiver:
def action(self):
print("Receiver.action()")
class Invoker:
def __init__(self):
self.commands = []
def add_command(self, command):
self.commands.append(command)
def execute_commands(self):
for command in self.commands:
command.execute()
# Usage
receiver = Receiver()
command = ConcreteCommand(receiver)
invoker = Invoker()
invoker.add_command(command)
invoker.execute_commands()