Skip to content

Singleton

Import packages

1
2
3
import sqlite3
import threading
from typing import Any, Dict, Type

Singleton

Ensure only one instance of a class exists, no matter how many times you instantiate it.

class Singleton(type):
    _instances: Dict[Type, Any] = {}
    _locks: Dict[Type, threading.Lock] = {}
    _global_lock: threading.Lock = threading.Lock()

    def __call__(cls, *args: Any, **kwargs: Any) -> Any:
        if cls in cls._instances:
            return cls._instances[cls]

        with cls._global_lock:
            lock = cls._locks.setdefault(cls, threading.Lock())

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

    @staticmethod
    def drop() -> None:
        Singleton._instances.clear()
        Singleton._locks.clear()

Usage

class Database(metaclass=Singleton):
    def __init__(self, db_url: str = ":memory:"):
        self.conn = sqlite3.connect(db_url)

    def query(self, sql: str):
        return self.conn.execute(sql).fetchall()


db1 = Database("database.sqlite")
db2 = Database("ignored.sqlite")

print(db1 is db2)

True

Multiton pattern

Ensure one instance per unique argument combination, as a cached object keyed by constructor arguments.

class SingletonHash(type):
    _instances: Dict[int, Any] = {}
    _locks: Dict[int, threading.Lock] = {}
    _global_lock: threading.Lock = threading.Lock()

    def __call__(cls, *args: Any, **kwargs: Any) -> Any:
        key = hash((cls, args, frozenset(kwargs.items())))

        if key in cls._instances:
            return cls._instances[key]

        with cls._global_lock:
            lock = cls._locks.setdefault(key, threading.Lock())

        with lock:
            if key not in cls._instances:
                cls._instances[key] = super().__call__(*args, **kwargs)

        return cls._instances[key]

    @staticmethod
    def drop() -> None:
        SingletonHash._instances.clear()
        SingletonHash._locks.clear()

Usage

class APIClient(metaclass=SingletonHash):
    def __init__(self, base_url: str):
        self.base_url = base_url


client1 = APIClient("https://service1.com")
client2 = APIClient("https://service1.com")
client3 = APIClient("https://service2.com")

print(client1 is client2)
print(client1 is client3)

True

False

class Config(metaclass=SingletonHash):
    def __init__(self, env: str):
        self.env = env


dev_config1 = Config("dev")
dev_config2 = Config("dev")
prod_config = Config("prod")

print(dev_config1 is dev_config2)
print(dev_config1 is prod_config)

True

False