Python Singleton Pattern

The singleton pattern is a commonly used software design pattern that ensures a class has only one instance and provides a global access point to obtain this instance.

Core Concepts

Imagine the CEO position in a company - the entire company can only have one CEO. No matter which department needs to report to the CEO, they all access the same CEO object. The singleton pattern is the "CEO position management mechanism" in the programming world.

Why Do We Need the Singleton Pattern?

In actual development, for some objects we only need one instance, such as:

  • Configuration manager: The entire application shares the same set of configurations
  • Database connection pool: Avoid repeatedly creating connections and save resources
  • Logger: Ensure all logs are written to the same file
  • Cache system: Manage cached data uniformly

Ways to Implement the Singleton Pattern

Python provides multiple ways to implement the singleton pattern. Let's learn them one by one from simple to complex.

Method 1: Using a Module (The Simplest Way)

Python modules themselves are naturally singletons. When a module is imported, it is initialized once, and subsequent imports will use the same instance.

Example

# singleton_module.py
class DatabaseConnection:
    def __init__(self):
        self.connection_string = "database://localhost:5432/mydb"
        print("Database connection created")
   
    def query(self, sql):
        return f"Executing query: {sql}"

# Create a singleton instance
db_instance = DatabaseConnection()

# Use it in other files:
# from singleton_module import db_instance
# result = db_instance.query("SELECT * FROM users")

Method 2: Using__new__the __new__ method

By overriding__new__the __new__ method to control the instantiation process.

Example

class Singleton:
    _instance = None
   
    def __new__(cls, *args, **kwargs):
        # If the instance does not exist, create a new one
        if not cls._instance:
            cls._instance = super().__new__(cls)
            print("Creating a new singleton instance")
        else:
            print("Returning the existing singleton instance")
        return cls._instance
   
    def __init__(self, name):
        # Note: __init__ is called every time
        self.name = name
        print(f"Initializing instance, name: {name}")

# Test code
print("=== Testing the Singleton Pattern ===")
s1 = Singleton("First instance")
s2 = Singleton("Second instance")

print(f"ID of s1: {id(s1)}")
print(f"ID of s2: {id(s2)}")
print(f"Are s1 and s2 the same object? {s1 is s2}")
print(f"s1 name: {s1.name}")
print(f"s2 name: {s2.name}")  # Note: "Second instance" will be shown here

Output result:

=== 测试单例模式 ===
创建新的单例实例
初始化实例,名称: 第一个实例
返回已存在的单例实例
初始化实例,名称: 第二个实例
s1 的 ID: 140245678945600
s2 的 ID: 140245678945600
s1 和 s2 是同一个对象吗? True
s1 名称: 第二个实例
s2 名称: 第二个实例

Method 3: Using a Decorator

Create a generic singleton decorator that can easily convert any class into a singleton.

Example

def singleton(cls):
    """Singleton decorator"""
    instances = {}
   
    def get_instance(*args, **kwargs):
        # If this class does not have an instance yet, create a new one
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
            print(f"Creating a new instance of {cls.__name__}")
        else:
            print(f"Returning the existing {cls.__name__} instance")
        return instances[cls]
   
    return get_instance

@singleton
class ConfigurationManager:
    def __init__(self):
        self.settings = {}
        self.load_default_settings()
   
    def load_default_settings(self):
        self.settings = {
            "app_name": "My application",
            "version": "1.0.0",
            "debug_mode": True
        }
   
    def get_setting(self, key):
        return self.settings.get(key)
   
    def set_setting(self, key, value):
        self.settings[key] = value

# Test code
print("\n"=== Testing the Decorator Singleton ===")
config1 = ConfigurationManager()
config2 = ConfigurationManager()

config1.set_setting("theme", "dark")
print(f"config1 theme: {config1.get_setting('theme')}")
print(f"config2 theme: {config2.get_setting('theme')}")  # The two instances share the same configuration

Method 4: Using a Metaclass (Advanced Usage)

A metaclass can control the creation process of classes, which is another powerful way to implement the singleton pattern.

Example

class SingletonMeta(type):
    """Singleton metaclass"""
    _instances = {}
   
    def __call__(cls, *args, **kwargs):
        # If this class does not have an instance yet, create a new one
        if cls not in cls._instances:
            instance = super().__call__(*args, **kwargs)
            cls._instances[cls] = instance
            print(f"Metaclass: Creating a new instance of {cls.__name__}")
        else:
            print(f"Metaclass: Returning the existing {cls.__name__} instance")
        return cls._instances[cls]

class Logger(metaclass=SingletonMeta):
    def __init__(self, log_file="app.log"):
        self.log_file = log_file
        self.logs = []
        print(f"Logger initialized, file: {log_file}")
   
    def log(self, message):
        log_entry = f"[{self.get_timestamp()}] {message}"
        self.logs.append(log_entry)
        print(f"Logging entry: {log_entry}")
        return log_entry
   
    def get_timestamp(self):
        from datetime import datetime
        return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
   
    def get_logs(self):
        return self.logs.copy()

# Test code
print("\n"=== Testing the Metaclass Singleton ===")
logger1 = Logger("application.log")
logger2 = Logger("different.log")  # This file name will be ignored

logger1.log("System startup")
logger2.log("User login")

print(f"logger1 log count: {len(logger1.get_logs())}")
print(f"logger2 log count: {len(logger2.get_logs())}")
print(f"Are they the same logger? {logger1 is logger2}")

Application Scenarios of the Singleton Pattern

Let's see the application of the singleton pattern in real projects through a complete example.

Practical Case: Application Configuration Manager

Example

class AppConfig:
    _instance = None
   
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._initialized = False
        return cls._instance
   
    def __init__(self):
        # Prevent repeated initialization
        if not self._initialized:
            self.config_data = {}
            self.load_config()
            self._initialized = True
   
    def load_config(self):
        """Simulate loading configuration from a configuration file"""
        self.config_data = {
            "database": {
                "host": "localhost",
                "port": 5432,
                "name": "myapp_db"
            },
            "server": {
                "host": "0.0.0.0",
                "port": 8000
            },
            "features": {
                "cache_enabled": True,
                "debug_mode": False
            }
        }
        print("Configuration loaded")
   
    def get(self, key_path, default=None):
        """Get configuration value by path, e.g., 'database.host'"""
        keys = key_path.split('.')
        value = self.config_data
       
        try:
            for key in keys:
                value = value[key]
            return value
        except (KeyError, TypeError):
            return default
   
    def set(self, key_path, value):
        """Set configuration value"""
        keys = key_path.split('.')
        config = self.config_data
       
        # Traverse to the key before the last one
        for key in keys[:-1]:
            if key not in config:
                config[key] = {}
            config = config[key]
       
        # Set the final value
        config[keys[-1]] = value
        print(f"Configuration updated: {key_path} = {value}")

# Usage example
def demonstrate_config_usage():
    print("\n"=== Configuration Manager Usage Demo ===")
   
    # Obtain the configuration manager instance in different places
    config1 = AppConfig()
    config2 = AppConfig()
   
    print(f"Are they the same configuration manager? {config1 is config2}")
   
    # Read configuration
    db_host = config1.get("database.host")
    server_port = config1.get("server.port")
    print(f"Database host: {db_host}")
    print(f"Server port: {server_port}")
   
    # Update configuration
    config2.set("database.host", "192.168.1.100")
    config2.set("features.debug_mode", True)
   
    # Verify configuration synchronization
    print(f"config1 database host: {config1.get('database.host')}")
    print(f"config1 debug mode: {config1.get('features.debug_mode')}")

# Run the demo
demonstrate_config_usage()

Considerations for the Singleton Pattern

Advantages

  1. Resource saving: Avoid repeatedly creating objects, saving memory and system resources
  2. Data consistency: All clients use the same instance, ensuring data consistency
  3. Global access: Provide a unified access point for ease of management

Disadvantages

  1. Global state: The singleton object holds global state, which may cause unexpected side effects
  2. Difficult testing: Due to global state, unit testing can become complicated
  3. Violates the Single Responsibility Principle: The singleton class must not only manage its own business logic but also control instantiation

Best Practices

Example

class ThreadSafeSingleton:
    """Thread-safe singleton pattern"""
    _instance = None
    _lock = threading.Lock()
   
    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                # Double-checked locking
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
                    print("Creating a thread-safe singleton instance")
        return cls._instance
   
    def __init__(self):
        # Ensure it is initialized only once
        if not hasattr(self, '_initialized'):
            self.data = {}
            self._initialized = True

Exercises and Thinking

Hands-on Exercises

  1. Implement a cache manager:

    • Create a singleton cache class
    • Support setting, getting, and deleting cache items
    • Add a cache expiration time feature
  2. Improve the configuration manager:

    • Add functionality to load configuration from a JSON file
    • Implement a configuration change listener
    • Add a configuration validation mechanism

Thought Questions

  1. In what situations should the singleton pattern be avoided?
  2. How does the singleton pattern affect the testability of code?
  3. What should be noted when using the singleton pattern in a multi-threaded environment?

Summary

The singleton pattern is a very important design pattern in Python. By ensuring that a class has only one instance, it provides unified management of resources. Through modules,__new__methods, decorators, metaclasses, and other implementation approaches, we can choose the most suitable method according to specific needs.

Remember, although the singleton pattern is very practical, it should be used with caution to avoid code coupling and testing difficulties caused by overuse. In real projects, the appropriate use of the singleton pattern can greatly improve code quality and maintainability.

Other Extensions