Какие знаешь способы создания Singleton?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Способы создания 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).
Для существующего кода:
- Простой случай → Module-based Singleton.
- Многопоточность → Metaclass или Lock.
- Декоратор → когда нужна переиспользуемость.
- Async → Contextvars.
В production я избегаю Singleton, где возможно, потому что это усложняет тестирование и масштабирование.