Паттерн Singleton (Одиночка) Python

Паттерн Singleton (Одиночка)

Одиночка (англ. singleton) - один из самых известных паттернов проектирования. Синглтон может создать только один экземпляр и предоставляет к нему глобальную точку доступа. Его удобно использовать для избежания конфликтующих запросов к общим ресурсам, например, к базам данных или файлам конфигурации. Одиночками, по сути, являются любые импортированные модули.

Как реализовать паттерн одиночка?

Как часто бывает с популярными паттернами, нет единого рецепта для создания одиночек. Для разных целей подойдут разные подходы, но у всех них есть общие принципы. В первую очередь, нужно ограничить доступ к конструктору класса. Этого можно добиться, переопределив магический метод __new__.

class Singleton:
    def __new__(cls, *args, **kwargs):
        if not hasattr(cls, 'instance'):
            cls.instance = super(Singleton, cls).__new__(cls)
        return cls.instance

    def __init__(self, ...):
        self.x = 0
        ...

s1 = Singleton()
s2 = Singleton()
print(s1 == s2) # True

Давайте разберемся, что здесь происходит. Метод __new__ вызывается до конструктора __init__. При первом вызове мы узнаём, что у класса нет аттрибута instance.

Затем мы создаем его. Туда мы запишем результат выполнения метода __new__, взятого у суперкласса. В нашем случае это базовый класс object, от которого неявно наследуются все остальные классы. Затем мы вернем полученное значение — адрес нашего объекта. Ту же последовательность действий, только без проверки существования аттрибута instance мы вызвали бы, не изменяя метод __new__. Дальше у нас запустится конструктор, и создаст первый объект, записав его по полученному адресу.

Теперь мы вызовем класс во второй раз. Поскольку аттрибут instance уже существует в нашем классе, мы просто возвращаем его. Таким образом, нельзя создать еще один экземпляр Singleton.

Другой вариант этой реализации — заранее создать приватный аттрибут __instance = None и перезаписывать его при первом вызове. Этот подход более надежен.

Используем метакласс

Попробуем перезаписать значение атрибута x нашего одиночки и снова вызвать класс.

s1 = Singleton()
s1.x = 10
print(s1.x)  # 10
s2 = Singleton()
print(s1.x)  # 0

Мы видим, что конструктор все равно перезапустился, и поменял значение x на значение по умолчанию. Избежать этой проблемы можно с помощью метакласса. Поскольку в Python и классы и их экземпляры являются объектами, то ничто не помешает нам создавать классы, создающие классы. Это и есть метаклассы. Базовым метаклассом является type, который вы могли использовать, чтобы проверять тип переменных.

class SingletonMetaclass(type):
    _instances = {}

    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 DataBase(metaclass=SingletonMetaclass):
    def __init__(self):
        self.file = "Database.sqlite3"

db1 = DataBase()
db1.file = "DB.sqlite3"
db2 = DataBase()
print(db1.file)
# DB.sqlite3

Итак, наш метакласс содержит защищенный словарь _instances для хранения адресов всех синглтонов. Магический метод __call__ будет запускаться при вызове экземпляра этого метакласса - то есть одиночки. Если синглтон еще не существует, он будет создан и записан в словарь. В любом случае нам вернется адрес единственного экземпляра класса.

Чтобы использовать метакласс в создании класса, мы должны явно прописать это, как в классе DataBase.

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

class Config(metaclass=SingletonMetaclass):
    ...

Заключение

Одиночка — очень полезный порождающий паттерн. Он предоставляет глобальную точку доступа и гарантирует, наличие не более одного своего экземпляра. Более сложные одиночки могут работать во многопоточной среде. С другой стороны, синглтон часто воспринимается как антипаттерн, поскольку создаёт неудобства при юнит-тестировании и не соответствуют принципу единой ответственности SOLID.

Практический Python для начинающих
Практический Python для начинающих

Станьте junior Python программистом за 7 месяцев

 7 месяцев

Возможно будет интересно

🏆 Hello, world! Python
Новичок
🏆 Hello, world!

Мы вчера запустили новый www.pylot.me. Должны были в следующую среду, но запустили вчера.

2022-10-04
Как практиковаться в Python? Python
Новичок
Как практиковаться в Python?

Для улучшения качества знаний и повышения уровня программиста, необходим постоянный практикум. Где можно это организовать самостоятельно, и как практиковаться в Python?

2022-10-19
Условные конструкции и сопоставление структурных шаблонов Шпаргалки
Новичок
Условные конструкции и сопоставление структурных шаблонов

Шпаргалка по условным конструкциям и сопоставлению структурных шаблонов

2022-11-09