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

Какие знаешь способы создания Singleton?

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

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

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

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

Способы создания Singleton в Python

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

1. Классический Singleton через переопределение new

Мост блокирует множественное создание экземпляра на уровне объекта.

class Singleton:
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

# Использование
s1 = Singleton()
s2 = Singleton()
assert s1 is s2  # True — один объект

Проблема: не потокобезопасен. В многопоточной среде может создаться несколько экземпляров.

2. Thread-safe Singleton с Lock

Добавляю блокировку для потокобезопасности.

import threading

class SingletonThreadSafe:
    _instance = None
    _lock = threading.Lock()
    
    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:  # Double-checked locking
                    cls._instance = super().__new__(cls)
        return cls._instance

# Тест
def create_singleton():
    s = SingletonThreadSafe()
    return id(s)

from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=10) as executor:
    results = list(executor.map(lambda _: create_singleton(), range(100)))
    assert len(set(results)) == 1  # Все id одинаковые

Плюсы: потокобезопасен, эффективен (lock только при первом создании). Минусы: verbose, требует импортов.

3. Singleton через Decorator

Элегантный и переиспользуемый способ.

import functools
from typing import Any, Callable, TypeVar

T = TypeVar("T")

def singleton(cls: type[T]) -> Callable[..., T]:
    """Декоратор для создания Singleton из класса."""
    instances = {}
    
    @functools.wraps(cls)
    def get_instance(*args: Any, **kwargs: Any) -> T:
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    
    return get_instance

# Использование
@singleton
class DatabaseConnection:
    def __init__(self, host: str):
        self.host = host
        print(f"Creating connection to {host}")

# Первый вызов создаёт объект
db1 = DatabaseConnection("localhost")
# Второй вызов возвращает тот же объект
db2 = DatabaseConnection("localhost")  # print не выполнится

assert db1 is db2  # True

Плюсы: чистый, переиспользуемый, работает с наследованием. Минусы: возвращает функцию, не класс (IDE не понимает типы).

4. Singleton через Metaclass

Метаклассы управляют созданием классов. Очень Pythonic.

class SingletonMeta(type):
    """Метакласс для Singleton."""
    _instances = {}
    _lock = threading.Lock()
    
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            with cls._lock:
                if cls not in cls._instances:
                    instance = super().__call__(*args, **kwargs)
                    cls._instances[cls] = instance
        return cls._instances[cls]

class Logger(metaclass=SingletonMeta):
    def __init__(self, name: str):
        self.name = name
        self.logs = []
    
    def log(self, message: str) -> None:
        self.logs.append(message)

# Использование
logger1 = Logger("app")
logger2 = Logger("app")
logger1.log("Event 1")
logger2.log("Event 2")

assert logger1 is logger2  # True
assert logger1.logs == ["Event 1", "Event 2"]  # True

# Наследование работает
class FileLogger(Logger):
    pass

file_logger1 = FileLogger("file")
file_logger2 = FileLogger("file")
assert file_logger1 is file_logger2  # True, но отдельный instance от Logger

Плюсы: потокобезопасен, поддерживает наследование, очень Pythonic. Минусы: сложнее для новичков, может затмить смысл кода.

5. Singleton через Module

Простейший способ — использовать сам модуль как Singleton.

# database.py
class DatabaseConnection:
    def __init__(self):
        self.connected = False
    
    def connect(self):
        self.connected = True
        print("Connected")

# Экземпляр создаётся один раз при импорте
db_instance = DatabaseConnection()

# Использование в других модулях
# main.py
from database import db_instance

db_instance.connect()

# Другой модуль
from database import db_instance

if db_instance.connected:  # True, тот же instance
    print("Already connected")

Плюсы: самый простой способ, Pythonic, потокобезопасен (благодаря GIL). Минусы: сложнее тестировать (нужно мокировать модуль).

6. Singleton через contextvars (для async)

Для асинхронного кода нужен особый подход.

import asyncio
from contextvars import ContextVar

class AsyncDatabasePool:
    _instance: ContextVar["AsyncDatabasePool"] = ContextVar(
        "db_pool", default=None
    )
    
    def __new__(cls):
        instance = cls._instance.get()
        if instance is None:
            instance = super().__new__(cls)
            cls._instance.set(instance)
        return instance
    
    async def query(self, sql: str):
        # Имитация запроса
        await asyncio.sleep(0.1)
        return f"Result: {sql}"

# Использование
async def main():
    pool1 = AsyncDatabasePool()
    pool2 = AsyncDatabasePool()
    assert pool1 is pool2
    
    result = await pool1.query("SELECT * FROM users")
    print(result)

asyncio.run(main())

Плюсы: потокобезопасен в async контексте, работает с asyncio. Минусы: только для async кода, специфичен.

Сравнение методов

МетодПростотаПотокобезопасностьТипизацияНаследованиеТестируемость
new
Lock⭐⭐
Decorator⭐⭐⭐⭐⭐
Metaclass
Module⭐⭐⭐⭐⭐⭐
Contextvars⭐⭐

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

Используй:

  • Логирование
  • БД подключение
  • Конфигурация приложения
  • Thread pools
  • Cache

НЕ используй:

  • Часто (обычно это признак плохого дизайна)
  • В микросервисах (используй dependency injection)
  • Если нужны разные конфигурации

Альтернатива: Dependency Injection

Модерный подход вместо Singleton:

from dependency_injector import containers, providers

class Container(containers.DeclarativeContainer):
    config = providers.Configuration()
    db = providers.Singleton(DatabaseConnection, config.host)
    logger = providers.Singleton(Logger)

# Использование
container = Container()
db = container.db()
logger = container.logger()

Плюсы: более гибко, легче тестировать, явно управляю зависимостями.

Мой выбор

Для новых проектов: Dependency Injection (dependency_injector, injector, pydantic.BaseSettings).

Для существующего кода:

  1. Простой случай → Module-based Singleton.
  2. Многопоточность → Metaclass или Lock.
  3. Декоратор → когда нужна переиспользуемость.
  4. Async → Contextvars.

В production я избегаю Singleton, где возможно, потому что это усложняет тестирование и масштабирование.

Какие знаешь способы создания Singleton? | PrepBro