What are Design patterns in Python?
Design patterns in Python are well-established solutions to common problems in software development, aiming to provide effective approaches in specific contexts. They offer guidance on how to structure and solve problems, while also highlighting why alternative options may not be suitable.
By applying design patterns, developers can benefit in various ways:
Speeding up development: Design patterns provide established structures and methodologies, reducing the time required to solve common problems. They offer reusable solutions that developers can apply directly, accelerating the development process.
Reducing code complexity: Design patterns often lead to more concise and readable code by eliminating unnecessary repetition. By following proven patterns, developers can write code that is easier to understand and maintain.
Ensuring well-designed code: Design patterns promote modular and flexible designs, enhancing the overall quality of the codebase. They help maintain a clear separation of concerns and improve the scalability and extensibility of the software.
Anticipating future issues: By adopting design patterns, developers can proactively address potential problems that may arise from small issues or design flaws. Design patterns are often based on collective experience and can help mitigate future challenges.
Design patterns are valuable for software developers regardless of the programming language they use.
1. Behavioral Design Patterns in Python
Behavioral design patterns in Python focus on the interaction and communication between objects, defining the patterns of communication and the responsibilities of objects. These patterns help in managing complex behavioral relationships, providing solutions for controlling the flow of communication, encapsulating algorithms, and handling complex behaviors efficiently.
Some common behavioral design patterns in Python include:
Iterator Pattern
State Pattern
Observer Pattern
1.1 Iterator Pattern
The iterator facilitates the traversal of elements in a collection without exposing the internal implementation details. Here are some key points about the Iterator pattern:
Use case: The Iterator pattern is commonly used to provide a standardized way of traversing collections.
Advantages:
Clean client code: By separating the responsibility of traversal from the collection, the Iterator pattern adheres to the Single Responsibility Principle, leading to cleaner and more focused client code.
Open/Closed Principle: Introducing iterators in collections can be done without modifying the client's code, making the system more extensible and following the Open/Closed Principle.
Independent iteration state: Each iterator object maintains its own iteration state, allowing for delayed or continued iteration as needed.
Drawbacks:
Potential application overload: When used with simple collections, the inclusion of iterators can introduce additional complexity and potentially impact the performance of the application. Care should be taken to assess the trade-off between the benefits of iterators and their potential impact on the system.
The Iterator pattern offers a structured approach to traversing collections while decoupling the iteration logic from the client code. It promotes modular and reusable designs, improving code maintainability and extensibility.
Structure
Code example
from __future__ import annotations
from collections.abc import Iterable, Iterator
from typing import Any, List
class AlphabeticalOrderIterator(Iterator):
_position: int = None
_reverse: bool = False
def __init__(self, collection: WordsCollection,
reverse: bool = False):
self._collection = collection
self._reverse = reverse
self._position = -1 if reverse else 0
def __next__(self):
try:
value = self._collection[self._position]
self._position += -1 if self._reverse else 1
except IndexError:
raise StopIteration()
return value
class WordsCollection(Iterable):
def __init__(self, collection: List[Any] = []):
self._collection = collection
def __iter__(self) -> AlphabeticalOrderIterator:
return AlphabeticalOrderIterator(self._collection)
def get_reverse_iterator(self) -> AlphabeticalOrderIterator:
return AlphabeticalOrderIterator(self._collection, True)
def add_item(self, item: Any):
self._collection.append(item)
if __name__ == "__main__":
collection = WordsCollection()
collection.add_item("First")
collection.add_item("Second")
collection.add_item("Third")
print("Straight traversal:")
print("\n".join(collection))
print("Reverse traversal:")
print("\n".join(collection.get_reverse_iterator()))
The code defines an AlphabeticalOrderIterator class that implements the Iterator interface, iterating over a WordsCollection in either forward or reverse order. The WordsCollection class implements the Iterable interface, providing methods to iterate over the collection and obtain a reverse iterator. In the main block, items are added to the collection, and both straight and reverse traversals are performed, showcasing the iterator functionality.
1.2 State Pattern
The State Pattern is a design pattern that enables an object to change its behavior based on internal state changes. It provides a structured approach to handle a large number of object states effectively.
Use case:
The State Pattern is useful in scenarios where an object needs to switch between a significant number of states. It helps simplify code by reducing duplication in similar state transitions and eliminates the need for extensive conditional statements.
Advantages:
Reduction of duplicate code: By separating code related to different states into distinct classes, the State Pattern eliminates the need for repetitive code blocks.
Avoidance of massive conditionals: Instead of complex conditional statements to handle different states, the State Pattern promotes a more organized and maintainable approach.
Follows the Single Responsibility Principle: Each state has its own class, allowing for better separation of concerns and ensuring that each class has a single responsibility.
Compliance with the Open/Closed Principle: The addition of new states do not require modifying existing classes or the overall context. The system remains open for extension without impacting the existing codebase.
Drawbacks:
Potential overhead: Implementing the State Pattern may introduce additional complexity when the state machine is relatively static and has few changes. It is important to consider the trade-off between flexibility and simplicity in such cases.
The State Pattern offers a structured approach to managing complex state transitions and behaviors in an object-oriented system. It helps promote code reusability, separation of concerns, and maintainability by encapsulating each state's logic within separate classes.
Structure
Code example
from __future__ import annotations
from abc import ABC, abstractmethod
class Context(ABC):
_state = None
def __init__(self, state: State):
self.transition_to(state)
def transition_to(self, state: State):
print(f"Context: Transition to {type(state).__name__}")
self._state = state
self._state.context = self
def request1(self):
self._state.handle1()
def request2(self):
self._state.handle2()
class State(ABC):
@property
def context(self) -> Context:
return self._context
@context.setter
def context(self, context: Context):
self._context = context
@abstractmethod
def handle1(self):
pass
@abstractmethod
def handle2(self):
pass
class ConcreteStateA(State):
def handle1(self):
print("ConcreteStateA handles request1.")
print("ConcreteStateA wants to change the state of the context.")
self.context.transition_to(ConcreteStateB())
def handle2(self):
print("ConcreteStateA handles request2.")
class ConcreteStateB(State):
def handle1(self):
print("ConcreteStateB handles request1.")
def handle2(self):
print("ConcreteStateB handles request2.")
print("ConcreteStateB wants to change the state of the context.")
self.context.transition_to(ConcreteStateA())
if __name__ == "__main__":
context = Context(ConcreteStateA())
context.request1()
context.request2()
The code defines an abstract State class and concrete implementations ConcreteStateA and ConcreteStateB. The Context class acts as a context for the state objects, allowing transitions between different states. In the main block, a Context object is created with an initial state of ConcreteStateA, and request1 and request2 methods are invoked on the context, demonstrating the handling of requests based on the current state.
1.3 Observer Pattern
The Observer pattern enables objects to receive notifications about events occurring in other objects they observe, without being tightly coupled to their classes.
One practical use case for the Observer pattern is when there is a need to implement a subscription mechanism, allowing objects to subscribe or unsubscribe from notifications related to events happening in a specific publisher class.
For example, consider a scenario where users can subscribe to news updates from an online magazine. They may have the option to select their areas of interest, such as science, digital technology, etc. This subscription mechanism can be implemented using the Observer pattern.
Similarly, in e-commerce platforms, you often encounter a "Notify me when it's in stock" button. When the desired item becomes available, subscribers who have opted for notifications will be notified using the Observer pattern.
Advantages:
The Observer pattern allows adding subscribers to receive notifications without modifying the publisher's code.
It enables a loosely coupled relationship between publishers and subscribers, promoting flexibility and modularity.
Disadvantages:
Subscribers may receive notifications in a random order, as they are notified as events occur.
Structure
Code example
from __future__ import annotations
from abc import ABC, abstractmethod
from random import randrange
from typing import List
class Subject(ABC):
@abstractmethod
def attach(self, observer: Observer):
pass
@abstractmethod
def detach(self, observer: Observer):
pass
@abstractmethod
def notify(self):
pass
class ConcreteSubject(Subject):
_state: int = None
_observers: List[Observer] = []
def attach(self, observer: Observer):
print("Subject: Attached an observer.")
self._observers.append(observer)
def detach(self, observer: Observer):
self._observers.remove(observer)
def notify(self):
print("Subject: Notifying observers...")
for observer in self._observers:
observer.update(self)
def some_business_logic(self):
print("Subject: I'm doing something important.")
self._state = randrange(0, 10)
print(f"Subject: My state has just changed to: {self._state}")
self.notify()
class Observer(ABC):
@abstractmethod
def update(self, subject: Subject):
pass
class ConcreteObserverA(Observer):
def update(self, subject: Subject):
if subject._state < 3:
print("ConcreteObserverA: Reacted to the event")
class ConcreteObserverB(Observer):
def update(self, subject: Subject):
if subject._state == 0 or subject._state >= 2:
print("ConcreteObserverB: Reacted to the event")
if __name__ == "__main__":
subject = ConcreteSubject()
observer_a = ConcreteObserverA()
subject.attach(observer_a)
observer_b = ConcreteObserverB()
subject.attach(observer_b)
subject.some_business_logic()
subject.some_business_logic()
subject.detach(observer_a)
subject.some_business_logic()
The code defines an abstract Subject class and a corresponding ConcreteSubject class that manages a list of observers and notifies them of any state changes. It also defines an abstract Observer class and two concrete observer classes (ConcreteObserverA and ConcreteObserverB) that react to specific state conditions.
In the main block, a ConcreteSubject object is created, along with two observer objects (observer_a and observer_b). The observer objects are attached to the subject, and then the subject's some_business_logic() method is called twice. After that, observer_a is detached from the subject, and the some_business_logic() method is called again.
2. Structural Design Patterns in Python
Structural design patterns in Python are a category of design patterns that focus on the composition and organization of classes and objects. These patterns provide solutions for creating relationships between objects and classes to form larger structures. They help in achieving flexible and reusable code by defining how objects and classes can interact and collaborate.
Structural design patterns define the relationships and structures between objects, allowing them to work together effectively to solve complex problems. These patterns emphasize the composition of objects and the relationships between them, rather than focusing on the specific implementation details.
Some commonly used structural design patterns are:
Facade Pattern
Decorator Pattern
Adapter Pattern
2.1 Facade Pattern
The Facade pattern offers a simplified interface that reduces the complexity of an application by providing a unified access point to a complex subsystem. It acts as a "mask" for the inner workings and intricacies of the subsystem.
One common use case for the Facade pattern is when dealing with complex libraries or APIs. By creating a Facade class, developers can encapsulate the complexity of the underlying system and expose only the necessary functionality to the client code.
Advantages:
System complexity is abstracted and isolated from the client code, making it easier to understand and work with.
The Facade pattern promotes a simplified and unified interface, allowing clients to interact with the subsystem using a consistent and streamlined approach.
Disadvantages:
There is a risk of creating a "god object" if the Facade class becomes too large and encompasses too much functionality. This can lead to code maintenance and scalability issues.
The Facade pattern is useful when dealing with complex subsystems, providing a simplified interface to interact with specific functionality while hiding the internal complexities. However, it is important to avoid excessive coupling and keep the Facade class focused and manageable to prevent the emergence of a bloated "god object.
Structure
Code example
class Addition:
def __init__(self, field1: int, field2: int):
self.field1 = field1
self.field2 = field2
def get_result(self):
return self.field1 + self.field2
class Multiplication:
def __init__(self, field1: int, field2: int):
self.field1 = field1
self.field2 = field2
def get_result(self):
return self.field1 * self.field2
class Subtraction:
def __init__(self, field1: int, field2: int):
self.field1 = field1
self.field2 = field2
def get_result(self):
return self.field1 - self.field2
class Facade:
@staticmethod
def make_addition(*args) -> Addition:
return Addition(*args)
@staticmethod
def make_multiplication(*args) -> Multiplication:
return Multiplication(*args)
@staticmethod
def make_subtraction(*args) -> Subtraction:
return Subtraction(*args)
if __name__ == "__main__":
addition_obj = Facade.make_addition(5, 5)
multiplication_obj = Facade.make_multiplication(5, 2)
subtraction_obj = Facade.make_subtraction(15, 5) print(addition_obj.get_result())
print(multiplication_obj.get_result())
print(subtraction_obj.get_result())
The above code defines three classes: Addition, Multiplication, and Subtraction, each representing a mathematical operation.
The Facade class acts as a simplified interface to these operations by providing static methods (make_addition(), make_multiplication(), and make_subtraction()) to create instances of the respective operation classes.
In the main block, the Facade class is used to create instances of the Addition, Multiplication, and Subtraction classes. The results of the operations are then printed.
2.2 Decorator Pattern
The Decorator pattern allows for attaching additional behaviors to objects dynamically, without modifying their underlying structure. This pattern involves creating a decorator class that wraps around the original object and introduces new functionality.
The Decorator pattern is commonly used when there is a need to enhance or extend the behavior of objects without directly altering their code.
Advantages:
Modifies the behavior of an object without the need to create subclasses or modify its original structure.
Multiple decorators can be combined, allowing for the composition of various behaviors by wrapping the object with different decorators.
Disadvantages:
Removing a specific decorator from the stack of wrappers can be challenging. Once a decorator is added, it becomes a part of the wrapper stack, and removing it individually may require additional complexity.
The Decorator pattern offers a flexible approach to adding or modifying behaviors of objects by wrapping them with decorators. It avoids the need for subclassing and enables the composition of multiple decorators to achieve desired combinations of functionality. However, careful consideration should be given to the management of decorators in case removal or alteration of specific decorators becomes necessary.
Structure
Code example
class my_decorator:
def __init__(self, func):
print("inside my_decorator.__init__()")
func() # Prove that function definition has completed def __call__(self):
print("inside my_decorator.__call__()")
@my_decorator
def my_function():
print("inside my_function()")
if __name__ == "__main__":
my_function()
The my_decorator class is defined as a decorator, and when the my_function function is defined, it is automatically wrapped with the my_decorator class. When my_function is called, the output will show the execution order: first the decorator's __init__() method, then the function's body, and finally the decorator's __call__() method.
2.3 Adapter Pattern
The Adapter pattern acts as a middle-layer class that facilitates the integration of independent or incompatible interfaces by providing a common interface between them.
The Adapter pattern is particularly useful when there is a need to establish collaboration between interfaces that have different formats or are not directly compatible.
A common use case for the Adapter pattern is when converting data formats. For instance, an Adapter can be used to convert XML data into JSON format, enabling further analysis or processing.
Advantages:
Separates the interface from the underlying business logic, promoting modularity and flexibility in the codebase.
Adding new adapters does not disrupt or break the client's existing code, as they can be integrated seamlessly.
Disadvantages:
Introducing adapters can increase code complexity, especially when dealing with multiple adapters and managing conversions between different interface formats.
The Adapter pattern serves as a bridge between interfaces with incompatible formats or functionalities. It allows for the separation of interfaces from business logic and enables the addition of new adapters without disrupting existing code. However, it is important to carefully manage the complexity that can arise from multiple adapters and interface conversions.
Structure
Code example
class Target:
def request(self):
return "Target: The default target's behavior."
class Adaptee:
def specific_request(self):
return ".eetpadA eht fo roivaheb laicepS"
class Adapter(Target, Adaptee):
def request(self):
return f"Adapter: (TRANSLATED) {self.specific_request()[::-1]}"
def client_code(target: "Target"):
print(target.request())
if __name__ == "__main__":
print("Client: I can work just fine with the Target objects:") target = Target()
client_code(target)
adaptee = Adaptee()
print("Client: The Adaptee class has a weird interface. "
"See, I don't understand it:")
print(f"Adaptee: {adaptee.specific_request()}")
print("Client: But I can work with it via the Adapter:")
adapter = Adapter()
client_code(adapter)
The above code creates a Target class with a default behavior, an Adaptee class with a specific behavior, and an Adapter class that adapts the Adaptee behavior to the Target interface. The client code shows that it can work seamlessly with Target objects, but when faced with the Adaptee class's weird interface, it utilizes the Adapter to work with it, achieving the desired results.
3. Creational Design Patterns in Python
Creational design patterns in Python are a category of design patterns that focus on object creation mechanisms. These patterns provide ways to create objects in a flexible and controlled manner, allowing developers to abstract the instantiation process and manage object creation based on specific requirements. Creational design patterns aim to promote code reusability, encapsulation, and decoupling. A common creational design pattern is Singleton Pattern.
3.1 Singleton Pattern
The Singleton pattern restricts a class from creating more than one instance and provides a global access point to that single instance.
The Singleton pattern is commonly used in scenarios where there is a need to manage a shared resource, such as a single database connection, file manager, or printer spooler, which needs to be accessed by multiple parts of an application. It is also useful for storing global states, like file paths, user language settings, or application paths. Additionally, it can be employed to create a simple logger for the entire application.
Advantages:
Ensures that a class has only one instance throughout the application.
Disadvantages:
Unit testing can become challenging, as many test frameworks rely on inheritance to create mock objects. Since Singletons cannot be subclassed or easily replaced, it can hinder unit testing practices.
The Singleton pattern provides a global point of access to a single instance of a class. It is useful for managing shared resources or storing global states. However, it is important to be mindful of the potential challenges it may present when it comes to unit testing due to its restriction on subclassing and object replacement.
Structure
Code example
class Singleton:
def __new__(cls):
if not hasattr(cls, 'instance'):
cls.instance = super(Singleton, cls).__new__(cls)
return cls.instance
if __name__ == "__main__":
s = Singleton()
print("Object created:", s)
s1 = Singleton()
print("Object created:", s1)
The __new__() method checks if the class already has an instance (stored in the class attribute instance). If an instance doesn't exist, it creates a new instance of the class using the super() function. The created instance is then stored in the instance attribute for future use.
When to use design patterns for Python?
Design patterns in Python are useful for solving common problems in software development. Here are some scenarios where specific design patterns can be applied:
Facade: When you need a unified interface to integrate multiple API options, such as integrating a payment system. The Facade pattern allows you to create a facade that can be easily replaced without rewriting the entire application. However, designing a common interface for facades can be challenging if the APIs are significantly different.
State: When you have multiple independent components in your application and want to manage their states. Creating a separate module for state management and using the Observer pattern can be beneficial in this case.
Decorator: This is a widely used pattern in Python due to the language's built-in support for decorators. Decorators provide a convenient way to enhance the functionality of libraries and enable flexible function composition, opening up possibilities in application design and management.
Adapter: When working with a large amount of data in different formats, the Adapter pattern allows you to use a single algorithm instead of multiple ones for each data format. It simplifies the data conversion process.
Iterator and Generator: These patterns are useful for efficiently working with large amounts of data. Iterator provides a way to traverse collections, while Generator, a variation of Iterator in Python, allows for more memory-efficient handling of data.
Singleton: When you need to ensure that only one instance of a class exists, such as managing a database connection, working with APIs, or file handling. The Singleton pattern helps maintain a clear process flow and reduces memory consumption by reusing the same instance instead of creating duplicates.
By understanding and applying these design patterns, developers can enhance their Python applications, improve code organization, and address common challenges more effectively.
It was an incredibly interesting article that I could not let go of, it touched me so much. I remember only one time I was so fascinated by something and it was Ninja, you can't even imagine how incredible life seems when you play in these places, it can be compared to some kind of fantasy, because I have no more words to describe the emotions you feel during this. So I advise everyone to try it and forget about this sad world for at least a few hours.