← Назад к вопросам

Как устроен паттерн Singleton?

2.2 Middle🔥 131 комментариев
#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Паттерн Singleton

Singleton — это порождающий паттерн проектирования, который гарантирует, что класс имеет только один экземпляр (инстанс), и предоставляет глобальную точку доступа к этому экземпляру.

Когда использовать Singleton

✓ Логирование
✓ Конфигурация приложения
✓ Пул подключений к БД
✓ Кэш приложения
✓ Очередь событий

✗ Не используй просто так, если нет необходимости в одном экземпляре
✗ Singleton может усложнить тестирование (зависимость от глобального состояния)

Реализация 1: Использование new

Это классический способ, когда мы контролируем создание объекта:

class Logger:
    _instance = None
    
    def __new__(cls):
        # __new__ вызывается перед __init__
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._initialized = False
        return cls._instance
    
    def __init__(self):
        # __init__ может быть вызван несколько раз
        # поэтому проверяем флаг инициализации
        if self._initialized:
            return
        
        self.logs = []
        self._initialized = True
    
    def add_log(self, message):
        self.logs.append(message)
    
    def get_logs(self):
        return self.logs

# Использование
logger1 = Logger()
logger2 = Logger()
logger3 = Logger()

logger1.add_log("Event 1")
logger2.add_log("Event 2")
logger3.add_log("Event 3")

# Все ссылаются на один объект
print(logger1 is logger2)  # True
print(logger1 is logger3)  # True
print(logger1.get_logs())  # ["Event 1", "Event 2", "Event 3"]

# Попытка создать "новый" экземпляр все равно вернёт тот же
logger4 = Logger()
print(logger4 is logger1)  # True

Реализация 2: Декоратор (рекомендуется)

Это более Pythonic способ и более гибкий:

def singleton(cls):
    """Декоратор для преобразования класса в Singleton"""
    instances = {}
    
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    
    return get_instance

@singleton
class Database:
    def __init__(self, host="localhost", port=5432):
        self.host = host
        self.port = port
        self.connected = False
    
    def connect(self):
        self.connected = True
        print(f"Подключено к {self.host}:{self.port}")
    
    def execute(self, query):
        if not self.connected:
            self.connect()
        return f"Выполнен запрос: {query}"

# Использование
db1 = Database()
db2 = Database()
db3 = Database()

print(db1 is db2)  # True
print(db1 is db3)  # True

db1.connect()
print(db2.execute("SELECT * FROM users"))  # Использует уже подключённую БД

Реализация 3: Метакласс (продвинутый способ)

Метаклассы позволяют контролировать создание самого класса:

class SingletonMeta(type):
    """
    Метакласс для создания Singleton классов.
    __call__ вызывается при создании инстанса.
    """
    _instances = {}
    _lock = {}  # Для потокобезопасности
    
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            # Создаём новый инстанс
            instance = super().__call__(*args, **kwargs)
            cls._instances[cls] = instance
        return cls._instances[cls]

class Config(metaclass=SingletonMeta):
    def __init__(self, filename="config.ini"):
        self.filename = filename
        self.settings = {}
    
    def load(self, key):
        # Симуляция загрузки конфигурации
        self.settings[key] = f"Value of {key}"
    
    def get(self, key):
        return self.settings.get(key)

# Использование
config1 = Config()
config2 = Config("other.ini")  # filename игнорируется, используется существующий инстанс

print(config1 is config2)  # True
config1.load("database_url")
print(config2.get("database_url"))  # "Value of database_url"

Реализация 4: Модульный уровень (самый простой)

Просто используй модуль как Singleton:

# logger.py
class _Logger:
    def __init__(self):
        self.logs = []
    
    def add_log(self, message):
        self.logs.append(message)
    
    def get_logs(self):
        return self.logs

# Создаём единственный экземпляр на уровне модуля
logger = _Logger()

# В других файлах просто импортируем
# from logger import logger
# logger.add_log("Event")

Потокобезопасный Singleton

Для многопоточных приложений нужна синхронизация:

import threading

class ThreadSafeSingleton:
    _instance = None
    _lock = threading.Lock()  # Мьютекс для синхронизации
    
    def __new__(cls):
        # Double-checked locking паттерн
        if cls._instance is None:
            with cls._lock:
                # Проверяем ещё раз после получения блокировки
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance
    
    def __init__(self):
        if not hasattr(self, 'initialized'):
            self.initialized = True
            self.data = []
    
    def add_data(self, item):
        self.data.append(item)
    
    def get_data(self):
        return self.data

# Потокобезопасное использование
def worker():
    singleton = ThreadSafeSingleton()
    for i in range(100):
        singleton.add_data(f"Item {i}")

threads = [threading.Thread(target=worker) for _ in range(5)]
for t in threads:
    t.start()
for t in threads:
    t.join()

singleton = ThreadSafeSingleton()
print(f"Всего элементов: {len(singleton.get_data())}")  # 500

Проблемы Singleton и их решение

# Проблема 1: Сложность тестирования
class BadLogger:
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
    
    def log(self, msg):
        print(msg)  # Сложно мокировать

# Решение: Dependency Injection
class GoodLogger:
    def log(self, msg):
        print(msg)

class Service:
    def __init__(self, logger=None):
        self.logger = logger or GoodLogger()  # Инъекция
    
    def do_work(self):
        self.logger.log("Working...")

# В тестах можем передать mock
class MockLogger:
    def __init__(self):
        self.messages = []
    
    def log(self, msg):
        self.messages.append(msg)

test_logger = MockLogger()
service = Service(test_logger)
service.do_work()
assert test_logger.messages == ["Working..."]

Проблема 2: Сокрытие зависимостей

# Плохо: Singleton скрывает зависимость
class BadDatabaseQuery:
    def execute(self):
        db = DatabaseSingleton.get_instance()
        return db.query()  # Скрытая зависимость!

# Хорошо: Явная зависимость
class GoodDatabaseQuery:
    def __init__(self, db):
        self.db = db  # Явная зависимость
    
    def execute(self):
        return self.db.query()  # Ясно, откуда берётся БД

Когда НЕ использовать Singleton

# Неправильно: Singleton для всего подряд
class UserSingleton:
    """Попытка создать Singleton для пользователя (плохо!)"""
    _instance = None
    
    def __new__(cls, user_id):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance.user_id = user_id
        return cls._instance

u1 = UserSingleton(1)
u2 = UserSingleton(2)
print(u1.user_id)  # 1, не 2!

# Правильно: Просто создавай несколько объектов
class User:
    def __init__(self, user_id):
        self.user_id = user_id

u1 = User(1)
u2 = User(2)
print(u1.user_id)  # 1
print(u2.user_id)  # 2

Резюме

  • new: Классический способ, работает с наследованием
  • Декоратор: Pythonic, простой и гибкий (рекомендуется)
  • Метакласс: Продвинутый способ, полный контроль
  • Модульный уровень: Самый простой для Python
  • Потокобезопасность: Используй блокировки или декораторы
  • Альтернатива: Часто можно обойтись без Singleton через Dependency Injection
  • Когда использовать: Логирование, конфигурация, пул подключений, кэш
Как устроен паттерн Singleton? | PrepBro