$ sudo teach IT
МОДУЛЬ 11 · УРОК 1

Singleton — Одиночка

Представьте: в вашем приложении есть конфиг с настройками. Один модуль создаёт конфиг с одними значениями, другой — с другими. Кто из них прав? Именно для таких ситуаций существует паттерн Singleton — гарантия того, что объект класса будет существовать в единственном экземпляре.

⏱ ~35 минут 🎓 Для новичков 🐍 Python 3 🧩 Паттерны проектирования

😱 Сначала — проблема, которую решает Singleton

Чтобы понять зачем нужен паттерн, нужно сначала почувствовать боль, которую он лечит. Давайте разберём конкретную ситуацию.

Допустим, у вас есть класс AppConfig — он хранит настройки приложения: адрес сервера, порт, режим отладки и так далее. Два разных модуля создают свой экземпляр этого класса и работают с ним:

class AppConfig:
    def __init__(self):
        self.debug = False
        self.db_host = "localhost"
        self.db_port = 5432

# Модуль A создаёт свой конфиг
config_a = AppConfig()
config_a.debug = True          # включили отладку
config_a.db_host = "prod.db"   # поменяли хост

# Модуль B создаёт свой конфиг
config_b = AppConfig()

# Что здесь произойдёт?
print(config_b.debug)    # False — НЕ True!
print(config_b.db_host)  # "localhost" — НЕ "prod.db"!

# Это РАЗНЫЕ объекты!
print(config_a is config_b)  # False

🔥 Вот и проблема: каждый раз, когда вы пишете AppConfig(), Python создаёт совершенно новый, независимый объект. Изменения в одном объекте никак не влияют на другой. В итоге разные части программы работают с разными конфигами — и это приводит к трудноуловимым багам.

Нам нужна гарантия: сколько раз бы мы ни написали AppConfig() — мы всегда получаем один и тот же объект. Именно это и делает паттерн Singleton (от англ. «single» — единственный).

🌍 Где Singleton применяется в реальной жизни

Прежде чем писать код, давайте поймём — а когда вообще нам нужен единственный объект? Вот классические примеры:

📋

Логгер (Logger)

Все части приложения должны писать в один лог-файл. Если каждый модуль создаст свой логгер — записи перемешаются или часть потеряется.

⚙️

Конфигурация (Config)

Настройки приложения должны быть одними для всех. Два разных объекта с разными настройками — источник хаоса.

🗄️

Пул соединений к БД

Соединение с базой данных дорогостоящее. Один менеджер соединений на всё приложение — экономит ресурсы.

🖥️

Кеш

Хранилище временных данных должно быть одним. Иначе разные части приложения будут видеть разные версии кеша.

💡 Общий принцип: Singleton нужен там, где ресурс или объект должен быть один на всё приложение — потому что дублирование либо физически невозможно, либо приводит к ошибкам.

🔧 Реализация через __new__ — питонический способ

Прежде чем писать код, давайте вспомним: как Python создаёт объекты? Когда вы пишете MyClass(), происходит два шага:

Шаг 1: __new__

Создание объекта — выделяет память и возвращает новый экземпляр. Это как «напечатать форму» под объект.

Шаг 2: __init__

Инициализация объекта — заполняет его начальными данными. Это как «заполнить форму» данными.

Обычно разработчики работают только с __init__. Но для Singleton нам нужно перехватить именно __new__ — момент создания объекта. Хитрость такая: если экземпляр уже существует — вернём старый. Если нет — создадим новый.

class AppConfig:
    _instance = None  # Здесь будем хранить единственный экземпляр

    def __new__(cls):
        # cls — это сам класс AppConfig
        if cls._instance is None:
            # Первый вызов: экземпляра ещё нет — создаём
            cls._instance = super().__new__(cls)
        # Второй, третий и все последующие вызовы:
        # экземпляр уже есть — просто возвращаем его
        return cls._instance

    def __init__(self):
        # ВНИМАНИЕ: __init__ вызывается каждый раз,
        # но мы вернули тот же объект — ничего страшного
        # Для простоты пока оставим так
        self.debug = False
        self.db_host = "localhost"
        self.db_port = 5432


# Проверяем!
config1 = AppConfig()
config2 = AppConfig()
config3 = AppConfig()

print(config1 is config2)  # True — один и тот же объект!
print(config2 is config3)  # True

# Меняем через config1 — видно через config2
config1.debug = True
print(config2.debug)  # True — они одно и то же!

print(id(config1) == id(id(config2)))  # True (id — адрес в памяти)

🔍 Разбираем по шагам:

  • _instance = None — атрибут класса (не объекта!). Хранится в самом классе, а не в каждом экземпляре.
  • __new__(cls) — вызывается до __init__, получает класс в качестве аргумента (а не self).
  • super().__new__(cls) — стандартный способ создать объект «по-честному», через родительский класс object.
  • Оператор is проверяет идентичность — одни ли это объекты в памяти (не просто равные, а один и тот же).

Теперь небольшое улучшение. Заметили проблему выше? __init__ вызывается при каждом обращении к AppConfig(), и каждый раз перезаписывает атрибуты. Это плохо, если мы что-то меняли. Исправим:

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):
        if self._initialized:
            return  # уже инициализировали — пропускаем
        self.debug = False
        self.db_host = "localhost"
        self.db_port = 5432
        self._initialized = True


config1 = AppConfig()
config1.debug = True     # меняем

config2 = AppConfig()    # создаём "новый" — но получаем тот же
print(config2.debug)     # True — __init__ не затёр наше значение!

✅ Это самый чистый и «питонический» способ реализации Singleton. Он встроен прямо в механизм создания объектов Python, не требует сторонних библиотек и хорошо читается.

🎀 Реализация через декоратор класса

Есть ещё один популярный способ — обернуть класс в декоратор. Идея: декоратор «перехватывает» создание объекта и контролирует, что существует только один экземпляр.

Если вы ещё не изучали декораторы — не пугайтесь. Воспринимайте это как «обёртку над классом, которая добавляет ему новое поведение».

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 DatabasePool:
    def __init__(self):
        self.connections = []
        self.max_connections = 10
        print("Пул соединений создан!")


pool1 = DatabasePool()  # Выведет: "Пул соединений создан!"
pool2 = DatabasePool()  # Ничего не выведет — экземпляр уже есть

print(pool1 is pool2)   # True

⚠️ Нюанс этого подхода

После применения декоратора DatabasePool уже не является «настоящим классом» — это функция get_instance. Поэтому такие вещи, как isinstance(pool1, DatabasePool) и наследование, работать не будут. Это ограничение декораторного подхода.

Декораторный подход хорош своей простотой и удобочитаемостью: достаточно написать @singleton над любым классом — и готово. Но если вам важно, чтобы класс оставался настоящим классом (с наследованием, isinstance и т.д.) — используйте вариант с __new__.

🐍 Pythonic way: модуль как Singleton

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

Когда Python импортирует модуль в первый раз — он его выполняет и сохраняет результат в кеш. При последующих импортах — просто возвращает закешированную версию. Один и тот же объект, всегда.

Значит, самый простой способ создать «синглтон-конфиг» в Python — это просто создать модуль settings.py и положить туда объект конфигурации:

# settings.py — этот файл и есть наш Singleton!

class _AppConfig:
    """Внутренний класс (с _ в начале) — сигнал, что его не надо использовать напрямую."""
    def __init__(self):
        self.debug = False
        self.db_host = "localhost"
        self.db_port = 5432
        self.secret_key = "my-secret-key"

# Создаём ОДИН экземпляр прямо в модуле
config = _AppConfig()
# module_a.py
from settings import config

config.debug = True
print(config.db_host)  # "localhost"
# module_b.py
from settings import config

# Это тот же объект, что и в module_a!
print(config.debug)   # True — мы видим изменение из module_a
print(config is config)  # очевидно True :)

🏆 Почему это считается лучшим способом в Python?

  • Никакого лишнего кода — нет __new__, нет декораторов
  • Работает за счёт встроенного механизма Python, а не «хаков»
  • Легко читается: from settings import config — сразу понятно что происходит
  • Django, Flask и другие фреймворки используют именно этот подход

Именно поэтому в реальных Python-проектах файл settings.py является стандартом — это и есть модульный синглтон в действии.

🔬 Реализация через метакласс — для знакомства

Есть ещё один способ — через метакласс. Метаклассы — это «классы для классов»: они контролируют, как создаются другие классы. Это продвинутая тема, которую мы разберём позже — сейчас просто посмотрим на код и поймём общую идею:

class SingletonMeta(type):
    """Метакласс, который делает любой класс Singleton'ом."""
    _instances = {}

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


class Logger(metaclass=SingletonMeta):
    def __init__(self):
        self.logs = []

    def log(self, message):
        self.logs.append(message)


log1 = Logger()
log2 = Logger()
print(log1 is log2)  # True

📌 Запомните только одно: метаклассовый подход — самый мощный и гибкий. Он лучше всего работает при множественном наследовании и в больших архитектурах. Но для начала — используйте __new__ или модульный синглтон.

⚠️ Почему Singleton считают антипаттерном

Честный разговор: в профессиональном сообществе Singleton — один из самых спорных паттернов. Его нередко называют антипаттерном — то есть «решением, которое выглядит как хорошая идея, но на деле создаёт проблемы».

Разберём главные претензии:

1. Глобальное состояние — источник хаоса

Singleton — это, по сути, глобальная переменная в красивой упаковке. Любая часть программы может изменить его состояние. Если что-то пошло не так — найти виновника крайне сложно: кто и когда поменял значение?

2. Сложно тестировать

Представьте: один тест меняет состояние Singleton, а следующий тест запускается и видит «грязные» данные от предыдущего. Тесты начинают зависеть от порядка запуска — это кошмар.

3. Нарушает принцип единственной ответственности

Класс не только делает свою работу (хранит конфиг), но и контролирует своё создание (следит, что экземпляр один). Две обязанности — потенциальный источник сложности.

🎓 Что это значит для вас? Не надо бояться Singleton или избегать его как огня. Нужно понимать его ограничения и применять осознанно — только там, где это действительно оправдано.

✅ Когда всё-таки стоит применять Singleton

Несмотря на критику, есть случаи, где Singleton — это именно то что нужно. Вот критерии:

🔒 Ресурс физически один

Например, соединение с базой данных или файл лога — здесь дублирование просто невозможно или вредно по природе самого ресурса.

📖 Состояние не меняется

Конфигурация, которую читают но не меняют в рантайме — отличный кандидат. Неизменяемое состояние убирает главную проблему глобальных данных.

⚡ Дорогая инициализация

Если создание объекта занимает время (загрузка файлов, установка сетевых соединений) — создать один раз и переиспользовать разумно.

Главный вопрос, который стоит задать себе перед применением Singleton: «Это действительно один ресурс на всё приложение по своей природе — или я просто хочу удобный глобальный доступ?» Если второе — ищите другое решение.

🎯 Итоги урока

🎯 Суть паттерна

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

🔧 Три способа реализации

Через __new__, через декоратор, через метакласс. Плюс Pythonic way — объект прямо в модуле.

🐍 Лучший Python-способ

Положить объект прямо в модуль (как в settings.py). Модуль сам по себе — синглтон.

⚠️ Осторожно!

Singleton = глобальное состояние. Применять осознанно, только когда ресурс действительно один по природе.