Data Science

🐍 Solid Design Patterns In Python Every Expert Uses Python Developer!

Hey there! Ready to dive into Solid 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!

SuperML Team
Share this article

Share:

🚀

💡 Pro tip: This is one of those techniques that will make you look like a data science wizard! Single Responsibility Principle (SRP) - Made Simple!

The Single Responsibility Principle states that a class should have only one reason to change, meaning it should have a single, well-defined responsibility or job. This principle promotes code organization, maintainability, and testability.

Don’t worry, this is easier than it looks! Here’s how we can tackle this:

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

    def display_book_info(self):
        print(f"{self.title}, Author: {self.author}")

class BookPersistence:
    def save_book(self, book, file_path):
        # Code to save book data to a file

    def load_book(self, file_path):
        # Code to load book data from a file

In this example, the Book class is responsible for holding book data, while the BookPersistence class handles the persistence of book data. Each class has a single responsibility, promoting code organization and maintainability.

🚀

🎉 You’re doing great! This concept might seem tricky at first, but you’ve got this! Open/Closed Principle (OCP) - Made Simple!

The Open/Closed Principle states that a class should be open for extension but closed for modification. In other words, you should be able to extend the behavior of a class without modifying its source code.

Let’s make this super clear! Here’s how we can tackle this:

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * self.radius ** 2

# Extension: Adding a new shape without modifying existing code
class Triangle(Shape):
    def __init__(self, base, height):
        self.base = base
        self.height = height

    def area(self):
        return 0.5 * self.base * self.height

In this example, the Shape class is an abstract base class with an abstract area method. The Rectangle and Circle classes inherit from Shape and implement the area method. To add a new shape (e.g., Triangle), we can simply create a new class that inherits from Shape and builds the area method, without modifying the existing code.

🚀

Cool fact: Many professional data scientists use this exact approach in their daily work! Liskov Substitution Principle (LSP) - Made Simple!

The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.

Let’s make this super clear! Here’s how we can tackle this:

class Vehicle:
    def start_engine(self):
        pass

class Car(Vehicle):
    def start_engine(self):
        print("Starting car engine.")

class ElectricCar(Car):
    def start_engine(self):
        print("Electric cars don't have engines.")

# Violates LSP
def start_vehicles(vehicles):
    for vehicle in vehicles:
        vehicle.start_engine()

car = Car()
electric_car = ElectricCar()
vehicles = [car, electric_car]
start_vehicles(vehicles)  # Raises an error for ElectricCar

In this example, the ElectricCar class violates the Liskov Substitution Principle because it overrides the start_engine method in an unexpected way, leading to a runtime error when used in place of a Car object. To adhere to LSP, we can refactor the code to use more appropriate method names or introduce additional abstractions.

🚀

🔥 Level up: Once you master this, you’ll be solving problems like a pro! Interface Segregation Principle (ISP) - Made Simple!

The Interface Segregation Principle states that a client should not be forced to depend on methods it does not use. Instead, interfaces should be split into smaller, more specific ones.

Let’s make this super clear! Here’s how we can tackle this:

from abc import ABC, abstractmethod

class Printer(ABC):
    @abstractmethod
    def print_document(self):
        pass

class Scanner(ABC):
    @abstractmethod
    def scan_document(self):
        pass

class MultiFunctionPrinter(Printer, Scanner):
    def print_document(self):
        # Code to print a document

    def scan_document(self):
        # Code to scan a document

class SimplePrinter(Printer):
    def print_document(self):
        # Code to print a document

In this example, the Printer and Scanner interfaces are segregated, allowing clients to depend only on the functionality they need. The MultiFunctionPrinter class builds both interfaces, while the SimplePrinter class only builds the Printer interface.

🚀 Dependency Inversion Principle (DIP) - Made Simple!

The Dependency Inversion Principle states that high-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.

Don’t worry, this is easier than it looks! Here’s how we can tackle this:

from abc import ABC, abstractmethod

class Logger(ABC):
    @abstractmethod
    def log(self, message):
        pass

class ConsoleLogger(Logger):
    def log(self, message):
        print(message)

class FileLogger(Logger):
    def __init__(self, file_path):
        self.file_path = file_path

    def log(self, message):
        with open(self.file_path, 'a') as file:
            file.write(message + '\n')

class PaymentProcessor:
    def __init__(self, logger: Logger):
        self.logger = logger

    def process_payment(self, amount):
        self.logger.log(f"Processing payment of ${amount}")
        # Payment processing code

console_logger = ConsoleLogger()
payment_processor = PaymentProcessor(console_logger)
payment_processor.process_payment(100)

In this example, the PaymentProcessor class depends on the Logger abstraction (interface), allowing flexibility to switch between different logging implementations without modifying the PaymentProcessor class. The ConsoleLogger and FileLogger classes implement the Logger interface, providing concrete logging implementations.

🚀 SRP Example: Separation of Concerns - Made Simple!

The Single Responsibility Principle promotes the separation of concerns, where each class or module has a single, well-defined responsibility.

Let me walk you through this step by step! Here’s how we can tackle this:

class BookStore:
    def __init__(self):
        self.books = []

    def add_book(self, book):
        self.books.append(book)

    def remove_book(self, book):
        self.books.remove(book)

    def display_books(self):
        for book in self.books:
            print(book)

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

    def __str__(self):
        return f"{self.title} by {self.author}"

# Usage
bookstore = BookStore()
book1 = Book("The Great Gatsby", "F. Scott Fitzgerald")
book2 = Book("Pride and Prejudice", "Jane Austen")

bookstore.add_book(book1)
bookstore.add_book(book2)
bookstore.display_books()

In this example, the BookStore class is responsible for managing the collection of books, while the Book class is responsible for representing a single book. Each class has a single responsibility, promoting code organization and maintainability.

🚀 OCP Example: Extension without Modification - Made Simple!

The Open/Closed Principle allows extending the functionality of a class without modifying its source code, promoting code reusability and maintainability.

Let’s break this down together! Here’s how we can tackle this:

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * self.radius ** 2

class AreaCalculator:
    def calculate_area(self, shapes):
        total_area = 0
        for shape in shapes:
            total_area += shape.area()
        return total_area

# Usage
rectangle = Rectangle(5, 3)
circle = Circle(2)
shapes = [rectangle, circle]

calculator = AreaCalculator()
total_area = calculator.calculate_area(shapes)
print(f"Total area: {total_area}")

In this example, the Shape class is an abstract base class with an abstract area method. The Rectangle and Circle classes inherit from Shape and implement the area method. The AreaCalculator class calculates the total area of a list of shapes without knowing their specific types. To add a new shape (e.g., Triangle), we can simply create a new class that inherits from Shape and builds the area method, without modifying the existing code.

🚀 LSP Example: Substitutability - Made Simple!

The Liskov Substitution Principle ensures that objects of a superclass can be replaced with objects of its subclasses without affecting the correctness of the program.

Let’s make this super clear! Here’s how we can tackle this:

class Vehicle:
    def start_engine(self):
        pass

    def drive(self):
        print("Driving...")

class Car(Vehicle):
    def start_engine(self):
        print("Starting car engine.")

class ElectricCar(Car):
    def start_engine(self):
        print("Electric cars don't have engines.")

def start_and_drive(vehicle):
    vehicle.start_engine()
    vehicle.drive()

# Usage
car = Car()
electric_car = ElectricCar()

start_and_drive(car)       # Output: Starting car engine. Driving...
start_and_drive(electric_car)  # Output: Electric cars don't have engines. Driving...

In this example, the ElectricCar class adheres to the Liskov Substitution Principle because it overrides the start_engine method in a way that still makes sense for its type. The start_and_drive function can work with both Car and ElectricCar objects without any issues.

🚀 ISP Example: Interface Segregation - Made Simple!

The Interface Segregation Principle promotes the separation of interfaces into smaller, more specific ones to avoid clients from depending on methods they don’t use.

Don’t worry, this is easier than it looks! Here’s how we can tackle this:

from abc import ABC, abstractmethod

class Printer(ABC):
    @abstractmethod
    def print_document(self):
        pass

class Scanner(ABC):
    @abstractmethod
    def scan_document(self):
        pass

class MultiFunctionPrinter(Printer, Scanner):
    def print_document(self):
        print("Printing document...")

    def scan_document(self):
        print("Scanning document...")

class SimplePrinter(Printer):
    def print_document(self):
        print("Printing document...")

def print_documents(printers):
    for printer in printers:
        printer.print_document()

# Usage
multi_function_printer = MultiFunctionPrinter()
simple_printer = SimplePrinter()

printers = [multi_function_printer, simple_printer]
print_documents(printers)

In this example, the Printer and Scanner interfaces are segregated, allowing clients to depend only on the functionality they need. The SimplePrinter class only builds the Printer interface, while the MultiFunctionPrinter class builds both Printer and Scanner interfaces. The print_documents function can work with any object that builds the Printer interface, without being affected by the presence or absence of the Scanner interface.

🚀 DIP Example: Decoupling with Abstractions - Made Simple!

The Dependency Inversion Principle promotes decoupling high-level and low-level modules by introducing abstractions that they both depend on.

Let me walk you through this step by step! Here’s how we can tackle this:

from abc import ABC, abstractmethod

class Logger(ABC):
    @abstractmethod
    def log(self, message):
        pass

class ConsoleLogger(Logger):
    def log(self, message):
        print(message)

class FileLogger(Logger):
    def __init__(self, file_path):
        self.file_path = file_path

    def log(self, message):
        with open(self.file_path, 'a') as file:
            file.write(message + '\n')

class PaymentProcessor:
    def __init__(self, logger: Logger):
        self.logger = logger

    def process_payment(self, amount):
        self.logger.log(f"Processing payment of ${amount}")
        print("Payment processed successfully.")

# Usage
console_logger = ConsoleLogger()
payment_processor = PaymentProcessor(console_logger)
payment_processor.process_payment(100)

file_logger = FileLogger("payment_logs.txt")
payment_processor = PaymentProcessor(file_logger)
payment_processor.process_payment(200)

In this example, the PaymentProcessor class depends on the Logger abstraction (interface) instead of a concrete implementation. This allows flexibility to switch between different logging implementations, such as ConsoleLogger or FileLogger, without modifying the PaymentProcessor class. The high-level module (PaymentProcessor) depends on the abstraction (Logger), adhering to the Dependency Inversion Principle.

🚀 SRP and OCP in Django - Made Simple!

The Single Responsibility Principle and Open/Closed Principle are commonly applied in the Django web framework through the separation of concerns and the use of modular design patterns.

Don’t worry, this is easier than it looks! Here’s how we can tackle this:

# models.py
from django.db import models

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.CharField(max_length=100)

# views.py
from django.shortcuts import render
from .models import Book

def book_list(request):
    books = Book.objects.all()
    return render(request, 'book_list.html', {'books': books})

# urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('books/', views.book_list, name='book_list'),
]

In this example, the Book model in models.py follows the Single Responsibility Principle by representing the book data. The views.py file handles the business logic for rendering the book list view. The urls.py file defines the URL patterns for the application. This separation of concerns promotes code organization, maintainability, and adherence to the Open/Closed Principle, as new functionality can be added without modifying existing code.

🚀 SOLID Principles in Practice - Made Simple!

Applying the SOLID principles in your Python projects can lead to more maintainable, extensible, and testable code. While the principles may seem abstract at first, practical examples and code reviews can help reinforce their understanding and application.

Here are some tips for incorporating SOLID principles into your projects:

  1. Conduct code reviews and refactoring sessions to identify potential violations and opportunities for improvement.
  2. Encourage discussion and knowledge sharing among team members to foster a deeper understanding of the principles.
  3. Continuously strive for simplicity and clarity in your code, as this often aligns with the goals of SOLID principles.
  4. Embrace design patterns and architectural patterns that promote adherence to SOLID principles, such as the Model-View-Controller (MVC) pattern or the Repository pattern.
  5. Continuously learn and practice by applying the principles in various projects, as the more you practice, the more natural it will become.

Remember, SOLID principles are not strict rules but rather guidelines to help you write better, more maintainable code. Applying them judiciously and adapting them to your specific project needs is key to reaping their benefits.

Certainly! Here’s an example that adheres to the Liskov Substitution Principle (LSP):

Let’s break this down together! Here’s how we can tackle this:

class Vehicle:
    def start(self):
        pass

    def drive(self):
        print("Driving...")

class Car(Vehicle):
    def start(self):
        print("Starting car engine.")

class ElectricCar(Vehicle):
    def start(self):
        print("Initializing electric motor.")

def start_and_drive(vehicle):
    vehicle.start()
    vehicle.drive()

# Usage
car = Car()
electric_car = ElectricCar()

start_and_drive(car)       # Output: Starting car engine. Driving...
start_and_drive(electric_car)  # Output: Initializing electric motor. Driving...

In this example, we have an abstract base class Vehicle with two methods: start and drive. The Car class inherits from Vehicle and overrides the start method to print “Starting car engine.” The ElectricCar class also inherits from Vehicle but overrides the start method to print “Initializing electric motor.”

The start_and_drive function takes a Vehicle object as an argument and calls its start and drive methods. When we call start_and_drive with a Car object, it prints “Starting car engine. Driving…” When we call it with an ElectricCar object, it prints “Initializing electric motor. Driving…”

By using a more generic method name start instead of start_engine, we avoid violating the Liskov Substitution Principle. Both Car and ElectricCar implement the start method in a way that makes sense for their respective types, and they can be used interchangeably without causing any unexpected behavior.

This example shows you how adhering to LSP can lead to more flexible and extensible code. If we need to add a new type of vehicle in the future, such as a hybrid car or a hydrogen-powered car, we can create a new subclass of Vehicle and implement the start method accordingly, without modifying the existing classes or the start_and_drive function.

🎊 Awesome Work!

You’ve just learned some really powerful techniques! Don’t worry if everything doesn’t click immediately - that’s totally normal. The best way to master these concepts is to practice with your own data.

What’s next? Try implementing these examples with your own datasets. Start small, experiment, and most importantly, have fun with it! Remember, every data science expert started exactly where you are right now.

Keep coding, keep learning, and keep being awesome! 🚀

Back to Blog

Related Posts

View All Posts »