Одиночка — это порождающий паттерн, который гарантирует существование только одного объекта определённого класса, а также позволяет достучаться до этого объекта из любого места программы.
Одиночка имеет такие же преимущества и недостатки, что и глобальные переменные. Его невероятно удобно использовать, но он нарушает модульность вашего кода.
Вы не сможете просто взять и использовать класс, зависящий от одиночки в другой программе. Для этого придётся эмулировать присутствие одиночки и там. Чаще всего эта проблема проявляется при написании юнит-тестов.
Применимость: Многие программисты считают Одиночку антипаттерном, поэтому его всё реже и реже можно встретить в 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