Одиночка — это порождающий паттерн, который гарантирует существование только одного объекта определённого класса, а также позволяет достучаться до этого объекта из любого места программы.
Одиночка имеет такие же преимущества и недостатки, что и глобальные переменные. Его невероятно удобно использовать, но он нарушает модульность вашего кода.
Вы не сможете просто взять и использовать класс, зависящий от одиночки в другой программе. Для этого придётся эмулировать присутствие одиночки и там. Чаще всего эта проблема проявляется при написании юнит-тестов.
Применимость: Многие программисты считают Одиночку антипаттерном, поэтому его всё реже и реже можно встретить в Python-коде.
Признаки применения паттерна: Одиночку можно определить по статическому создающему методу, который возвращает один и тот же объект.
Наивный Одиночка (небезопасный в многопоточной среде)
Топорно реализовать Одиночку очень просто — достаточно скрыть конструктор и предоставить статический создающий метод.
Тот же класс ведёт себя неправильно в многопоточной среде. Несколько потоков могут одновременно вызвать метод получения Одиночки и создать сразу несколько экземпляров объекта.
main.py: Пример структуры паттерна
class SingletonMeta(type):
"""
В Python класс Одиночка можно реализовать по-разному. Возможные способы
включают себя базовый класс, декоратор, метакласс. Мы воспользуемся
метаклассом, поскольку он лучше всего подходит для этой цели.
"""
_instances = {}
def __call__(cls, *args, **kwargs):
"""
Данная реализация не учитывает возможное изменение передаваемых
аргументов в `__init__`.
"""
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class Singleton(metaclass=SingletonMeta):
def some_business_logic(self):
"""
Наконец, любой одиночка должен содержать некоторую бизнес-логику,
которая может быть выполнена на его экземпляре.
"""
# ...
if __name__ == "__main__":
# Клиентский код.
s1 = Singleton()
s2 = Singleton()
if id(s1) == id(s2):
print("Singleton works, both variables contain the same instance.")
else:
print("Singleton failed, variables contain different instances.")
Output.txt: Результат выполнения
Singleton works, both variables contain the same instance.
Многопоточный Одиночка
Чтобы исправить проблему, требуется синхронизировать потоки при создании объекта-Одиночки.
main.py: Пример структуры паттерна
from threading import Lock, Thread
class SingletonMeta(type):
"""
Это потокобезопасная реализация класса Singleton.
"""
_instances = {}
_lock: Lock = Lock()
"""
У нас теперь есть объект-блокировка для синхронизации потоков во время
первого доступа к Одиночке.
"""
def __call__(cls, *args, **kwargs):
"""
Данная реализация не учитывает возможное изменение передаваемых
аргументов в `__init__`.
"""
# Теперь представьте, что программа была только-только запущена.
# Объекта-одиночки ещё никто не создавал, поэтому несколько потоков
# вполне могли одновременно пройти через предыдущее условие и достигнуть
# блокировки. Самый быстрый поток поставит блокировку и двинется внутрь
# секции, пока другие будут здесь его ожидать.
with cls._lock:
# Первый поток достигает этого условия и проходит внутрь, создавая
# объект-одиночку. Как только этот поток покинет секцию и освободит
# блокировку, следующий поток может снова установить блокировку и
# зайти внутрь. Однако теперь экземпляр одиночки уже будет создан и
# поток не сможет пройти через это условие, а значит новый объект не
# будет создан.
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class Singleton(metaclass=SingletonMeta):
value: str = None
"""
Мы используем это поле, чтобы доказать, что наш Одиночка действительно
работает.
"""
def __init__(self, value: str) -> None:
self.value = value
def some_business_logic(self):
"""
Наконец, любой одиночка должен содержать некоторую бизнес-логику,
которая может быть выполнена на его экземпляре.
"""
def test_singleton(value: str) -> None:
singleton = Singleton(value)
print(singleton.value)
if __name__ == "__main__":
# Клиентский код.
print("If you see the same value, then singleton was reused (yay!)\n"
"If you see different values, "
"then 2 singletons were created (booo!!)\n\n"
"RESULT:\n")
process1 = Thread(target=test_singleton, args=("FOO",))
process2 = Thread(target=test_singleton, args=("BAR",))
process1.start()
process2.start()
Output.txt: Результат выполнения
If you see the same value, then singleton was reused (yay!)
If you see different values, then 2 singletons were created (booo!!)
RESULT:
FOO
FOO