Full analysis of Python design patterns

Friends who have just jumped from static languages ​​to Python, do you think as soon as you look at the design pattern: "How can the code size be reduced by half and still be able to do fancy things?"

Design patterns are general solutions to common problems in software design. In Python, we take advantage of its dynamic nature to implement these patterns in simpler code than in Java. 👉 Complete Python design pattern code set: faif/python-patterns

This article will cover the most commonly used and Pythonic modes among the three categories: Creative, Structural, and Behavioral. Each mode is equipped with a minimalist code example so that you can get started after reading it.


1. Creational Patterns

Core Goal: Completely decouple these three things from the business logic of the system: "Who creates the object", "How to create it", and "What does it represent" - so that when the implementation is changed, the caller does not need to change a single line of code.

1. Factory Method

Separately encapsulate the logic of "creating an object" into a function or class, and expose an "on-demand pickup" entrance to the caller.

Python minimalist implementation

Even classes are omitted here, and functions are directly used to return lambda or built-in classes:

def get_serializer(fmt: str):
    """根据格式返回对应的序列化工具"""
    if fmt == "json":
        return lambda data: f"JSON: {str(data).replace(' ', '')}"
    if fmt == "xml":
        return lambda data: f"<xml>{data}</xml>"
    # 默认返回字符串
    return str

# 业务调用方
if __name__ == "__main__":
    user = {"name": "Alice", "age": 28}
    json_serializer = get_serializer("json")
    print(json_serializer(user))

✨ Advantages

  • Shield creation details, the caller only cares about "what I want".
  • To add a new type, you only need to add a branch to the factory, which complies with the opening and closing principle.

⚠️ Disadvantages

  • In simple scenarios (always returning only one type) it will appear over-designed.

🎯 Applicable scenarios

  • Dynamically select instances based on configuration, user input, or environment variables.
  • API response format switching (JSON/XML/Protobuf).

2. Abstract Factory

An upgraded version of the factory method - manages a set of interrelated objects that must be used in pairs (such as background color, text color, button style in "dark mode").

Python minimalist implementation

Taking advantage of Python's dynamic features, even the abstract base class can be omitted (it is recommended to retain it for large projects)abcconstraint):

# 定义两个「产品族」
class DarkComponent:
    get_bg = lambda self: "#000000"
    get_text = lambda self: "#FFFFFF"

class LightComponent:
    get_bg = lambda self: "#FFFFFF"
    get_text = lambda self: "#000000"

# 抽象工厂入口
def get_theme_factory(mode: str):
    if mode == "dark":
        return DarkComponent
    if mode == "light":
        return LightComponent
    raise ValueError(f"不支持的主题模式: {mode}")

# 业务调用方
if __name__ == "__main__":
    theme_factory = get_theme_factory("dark")
    print(f"背景色: {theme_factory.get_bg()}, 文字色: {theme_factory.get_text()}")

✨ Advantages

  • Force product families to be used together to ensure uniform style/function.
  • Switching product families is like switching a factory object.

⚠️ Disadvantages

  • Adding a new product family requires modifying all factory codes, and the expansion cost is high.

🎯 Applicable scenarios

  • UI skinning system (dark/light/high contrast themes).
  • Multi-database driver adaptation (MySQL/PostgreSQL/Oracle).
  • Multi-language translation component switching.

3. Lazy Initialization (Lazy Evaluation)

Objects or data are not prepared in advance and are only loaded when they are actually used for the first time - especially suitable for scenarios where "memory/resources are expensive and may never be used".

Python minimalist implementation

use@propertyDecorators hide loading logic behind property access:

class LargeFileLoader:
    def __init__(self, file_path: str):
        self.file_path = file_path
        self._content = None

    @property
    def content(self):
        """第一次访问时才加载文件"""
        if self._content is None:
            print(f"⏳ 首次加载大文件: {self.file_path}...")
            # 模拟加载:读取大文件并拆成列表
            self._content = ["行1", "行2", "行3", "行..."] * 10000
        return self._content

# 使用
loader = LargeFileLoader("超大日志.txt")
print("对象已初始化,文件未加载")
print(loader.content[:2])    # 触发加载
print(len(loader.content))   # 直接使用缓存

✨ Advantages

  • Reduce startup time and initial memory usage.
  • There is no need for external callers to care about "whether it has been initialized".

⚠️ Disadvantages

  • There is a noticeable delay on the first visit.
  • Natural threads are not safe (may be loaded repeatedly under multi-threading) and need to be locked.

🎯 Applicable scenarios

  • Load very large files or parse complex configurations.
  • Establish expensive database/network connections.
  • Import large data processing modules on demand.

4. Singleton mode (Singleton)

Ensure that the class has only one instance throughout the entire program life cycle - suitable for components that require "global unique shared state".

Python minimalist implementation

Using metaclasses to control the instance creation process is concise and relatively safe:

class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class AppConfig(metaclass=SingletonMeta):
    def __init__(self):
        print("🔧 首次加载配置...")
        self.db_host = "localhost"
        self.db_port = 3306

# 测试
config1 = AppConfig()
config2 = AppConfig()
print(config1 == config2)          # True
print(id(config1), id(config2))    # 地址相同

✨ Advantages

  • Save memory and ensure that the global state is unique.
  • Avoid side effects caused by repeated initialization.

⚠️ Disadvantages

  • Violates the single responsibility principle (the class must manage both business and singleton control).
  • Difficulty in testing (global state easily causes cross-contamination of tests).
  • Multi-threaded environments require additional locking.

🎯 Applicable scenarios

  • Global configuration management.
  • Database connection pool.
  • Logger.

2. Structural Patterns

Core Goal: Through class inheritance/object combination, scattered small classes can be flexibly assembled into a more powerful structure - you can add functions and change structures without changing the original code.

5. Decorator mode (Decorator)

Dynamically add extra functionality to an object or function - more flexible than creating a subclass, and you can add multiple decorators in any order.

Python native support!

use@Syntactic sugar, implement function decorator:

def bold(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return f"<b>{result}</b>"
    return wrapper

def italic(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return f"<i>{result}</i>"
    return wrapper

# 装饰器叠加顺序:从下往上生效
@bold
@italic
def say_hello(name: str):
    return f"Hello, {name}!"

print(say_hello("Bob"))   # <b><i>Hello, Bob!</i></b>

✨ Advantages

  • Dynamic overlay function without changing the original function code.
  • Highly compliant with the opening and closing principle, the decorators can be freely combined.

⚠️ Disadvantages

  • Multiple levels of nesting reduce debugging readability.
  • May lose meta-information of the original function (availablefunctools.wrapsreserve).

🎯 Applicable scenarios

  • Logging, performance monitoring, and permission verification.
  • Data formatting (e.g. HTML escaping, encryption).
  • Cache warm-up and retry mechanism.

3. Behavioral Patterns

Core Goal: Focus on "how objects communicate" and "how responsibilities are allocated" - making code communication clearer, responsibilities more single, and expansion easier.

6. Iterator pattern (Iterator)

Access the elements in the collection sequentially, but completely hide the internal implementation - Python native containers basically support it.

Python native usage + custom iterator

# 1. 原生用法
print("--- 原生 list 迭代器 ---")
my_list = [10, 20, 30]
it = iter(my_list)
print(next(it))   # 10
print(next(it))   # 20
print(next(it))   # 30

# 2. 自定义偶数迭代器
class EvenNumberIterator:
    def __init__(self, max_num: int):
        self.max_num = max_num
        self.current = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.current > self.max_num:
            raise StopIteration
        result = self.current
        self.current += 2
        return result

print("\n--- 自定义偶数迭代器 ---")
for num in EvenNumberIterator(10):
    print(num)

✨ Advantages

  • unificationfor ... ininterface, does not expose internal storage structures.
  • Supports lazy evaluation and can generate data on demand.

⚠️ Disadvantages

  • for simplelist / dict, handwritten iterators appear redundant.
  • Pay attention to handlingStopIteration, otherwise it will cause an infinite loop.

🎯 Applicable scenarios

  • Traverse the large table records in the database (return one by one, not load all at once).
  • Handle large streaming files.
  • Customize traversal of complex data structures (trees, graphs, linked lists).

Summarize

The above six patterns are the most commonly used design pattern models in Python projects and best reflect the simplicity of the language. The code given in this article removes lengthy abstract classes and interfaces, and uses functions, closures, metaclasses,@propertyWait for Python features to be replaced, allowing you to implement classic mode with minimal template code.

I hope this tutorial can help you quickly understand the essence of these patterns and apply them flexibly in actual development. If you want to dive into more examples (agents, observers, commands, etc.), you are welcome to go to faif/python-patterns to continue exploring.