Паттерн 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.
Возможно будет интересно
🏆 Hello, world!
Мы вчера запустили новый www.pylot.me. Должны были в следующую среду, но запустили вчера.
Как практиковаться в Python?
Для улучшения качества знаний и повышения уровня программиста, необходим постоянный практикум. Где можно это организовать самостоятельно, и как практиковаться в Python?
Условные конструкции и сопоставление структурных шаблонов
Шпаргалка по условным конструкциям и сопоставлению структурных шаблонов