Одинак — це породжуючий патерн, який гарантує існування тільки одного об’єкта певного класу, а також дозволяє дістатися цього об’єкта з будь-якого місця програми.
Одинак має такі ж переваги та недоліки, що і глобальні змінні. Його неймовірно зручно використовувати, але він порушує модульність вашого коду.
Ви не зможете просто взяти і використовувати клас, залежний від одинака, в іншій програмі. Для цього доведеться емулювати там присутність одинака. Найчастіше ця проблема проявляється при написанні юніт-тестів.
Застосування: Багато програмістів вважають Одинака антипатерном, тому його все рідше і рідше можна зустріти в Python-коді.
Ознаки застосування патерна: Одинака можна визначити за статичним створюючим методом, який повертає один і той же об’єкт.
Наївний Одинак (некоректний у багатопоточному середовищі)
Незграбно реалізувати Одинака дуже просто — достатньо приховати конструктор і надати створюючий статичний метод.
Той же клас веде себе неправильно в багатопотоковому середовищі. Декілька потоків можуть одночасно викликати метод отримання Одинака та створити відразу кілька екземплярів об’єкта.
main.py: Приклад структури патерна
class SingletonMeta(type):
"""
The Singleton class can be implemented in different ways in Python. Some
possible methods include: base class, decorator, metaclass. We will use the
metaclass because it is best suited for this purpose.
"""
_instances = {}
def __call__(cls, *args, **kwargs):
"""
Possible changes to the value of the `__init__` argument do not affect
the returned instance.
"""
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):
"""
Finally, any singleton should define some business logic, which can be
executed on its instance.
"""
# ...
if __name__ == "__main__":
# The client code.
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):
"""
This is a thread-safe implementation of Singleton.
"""
_instances = {}
_lock: Lock = Lock()
"""
We now have a lock object that will be used to synchronize threads during
first access to the Singleton.
"""
def __call__(cls, *args, **kwargs):
"""
Possible changes to the value of the `__init__` argument do not affect
the returned instance.
"""
# Now, imagine that the program has just been launched. Since there's no
# Singleton instance yet, multiple threads can simultaneously pass the
# previous conditional and reach this point almost at the same time. The
# first of them will acquire lock and will proceed further, while the
# rest will wait here.
with cls._lock:
# The first thread to acquire the lock, reaches this conditional,
# goes inside and creates the Singleton instance. Once it leaves the
# lock block, a thread that might have been waiting for the lock
# release may then enter this section. But since the Singleton field
# is already initialized, the thread won't create a new object.
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
"""
We'll use this property to prove that our Singleton really works.
"""
def __init__(self, value: str) -> None:
self.value = value
def some_business_logic(self):
"""
Finally, any singleton should define some business logic, which can be
executed on its instance.
"""
def test_singleton(value: str) -> None:
singleton = Singleton(value)
print(singleton.value)
if __name__ == "__main__":
# The client code.
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