Шаблон Singleton (Одиночка) один из самых часто используемых шаблонов. Его можно встретить во множестве проектов и он относительно прост для обучения. Его обязательно нужно знать и уметь его использовать.
Статья написана на основе книги: Python: Master the Art of Design Patterns (Dusty Phillips, Chetan Giridhar, Sakis Kasampalis)
В этой статье будет кратко рассмотрены следующие темы:
• Как устроен Singleton
• Реальный пример паттерна Singleton
• Реализация шаблона Singleton в Python
• Шаблон Monostate
Шаблон Singleton предоставляет механизм создания одного и только один экземпляра объекта, и предоставление к нему глобальную точку доступа. Поэтому, Singletons обычно используются в таких случаях, как ведение журнала или операции с базой данных, диспетчера очереди печати и многих других, где существует необходимость иметь только один экземпляр, который доступен во всем приложении, чтобы избежать конфликтующих запросов на один и тот же ресурс. Например, мы можем захотеть использовать один объект базы данных для выполнения операций с БД для обеспечения согласованности данных или один объект класса ведения журнала для нескольких служб, чтобы последовательно выгружать сообщения журнала в определенный файл журнала.
Вкратце, цель шаблона Singleton заключаются в следующем:
• Обеспечение создания одного и только одного объекта класса
• Предоставление точки доступа для объекта, который является глобальным для программы
• Контроль одновременного доступа к ресурсам, которые являются общими
Простой способ реализации Singleton — сделать закрытым метод конструктора и создать статический метод, который выполняет инициализацию объекта. Таким образом, один объект создается при первом вызове, а класс будет всегда возвращать тот же объект при попытки новой инициализации объекта.
В Python мы реализуем эту идею по-другому, поскольку в нем нет возможности создавать приватные конструкторы.
Вот пример кода шаблона Singleton в Python v3.5. В этом примере мы делаем две основные вещи:
class Singleton(object): def __new__(cls): if not hasattr(cls, 'instance'): cls.instance = super(Singleton, cls).__new__(cls) return cls.instance s = Singleton() print("Object created", s) s1 = Singleton() print("Object created", s1)
Вывод выполнения кода должен быть примерно таким:
('Object created', <__main__.Singleton object at 0x10ba9db90>) ('Object created', <__main__.Singleton object at 0x10ba9db90>)
В этом фрагменте кода мы переопределяем метод __new__ (специальный метод Python для создания объектов), что бы управлять созданием объекта. Объект s создается с помощью метода __new__, но перед этим он проверяет, существует ли уже созданный объект. Метод hasattr (специальный метод Python, позволяющий определить, имеет ли объект определенное свойство), используется для проверки наличия у объекта cls свойства instance. При создание объекта s, объект просто создается. В случае создания объекта s1, hasattr() обнаруживает, что у объекта уже существует свойство instance, и, следовательно, s1 использует уже существующий экземпляр объекта (расположенный по адресу 0x10ba9db90).
Одним из вариантов использования шаблона Singleton является отложенная инициализация. Например, в случае импорта модулей мы можем автоматически создать объект, даже если он не нужен. Отложенное создание экземпляра гарантирует, что объект создается, только тогда, когда он действительно необходим.
В следующем примере кода, когда мы используем s = Singleton(), вызывается метод __init__, но при этом новый объект не будет создан. Фактическое создание объекта произойдет, когда мы используем Singleton.getInstance().
class Singleton: __instance = None def __init__(self): if not Singleton.__instance: print(" __init__ method called..") else: print("Instance already created:", self.getInstance()) @classmethod def getInstance(cls): if not cls.__instance: cls.__instance = Singleton() return cls.__instance s = Singleton() ## class initialized, but object not created print("Object created", Singleton.getInstance()) # Object gets created here s1 = Singleton() ## instance already created
Все модули по умолчанию являются синглетонами из-за особенностей работы импорта в Python. Python работает следующим образом:
В описание шаблона Singleton в книге Gang of Four говорится, что должен быть один и только один объект класса. Однако, согласно Алексу Мартелли, программисту обычно требуется, чтобы экземпляры имели одно и то же состояние. Он предлагает разработчикам больше беспокоиться о состоянии и поведении, а не об идентичности экземпляров. Поскольку концепция основана на том что бы все объекты имели одно и то же состояние, она также известна как шаблон Monostate.
Шаблон Monostate может быть реализован очень простым способом. В коде ниже мы присваиваем переменную __dict__ (специальную переменную Python) переменной класса __shared_state. Python использует __dict__ для хранения состояния каждого объекта класса. В следующем коде мы намеренно назначаем __shared_state всем созданным экземплярам. Поэтому, когда мы создаем два экземпляра, «b» и «b1», мы получаем два разных объекта. Однако состояния переменных b.__dict__ и b1.__dict__ одинаковы. Теперь, даже если переменная объекта x изменится в объекте b, изменение копируется в переменную __dict__, которая является общей для всех объектов, и b1 получит это изменение:
class Borg: __shared_state = {"1": "2"} def __init__(self): self.x = 1 self.__dict__ = self.__shared_state pass b = Borg() b1 = Borg() b.x = 4 print("Borg Object 'b': ", b) ## b and b1 are distinct objects print("Borg Object 'b1': ", b1) print("Object State 'b':", b.__dict__)## b and b1 share same state print("Object State 'b1':", b1.__dict__)
В результате должно получится что то типа такого:
("Borg Object 'b': ", <__main__.Borg instance at 0x10baa5a70>) ("Borg Object 'b1': ", <__main__.Borg instance at 0x10baa5638>) ("Object State 'b':", {'1': '2', 'x': 4}) ("Object State 'b1':", {'1': '2', 'x': 4})
Другой способ реализации класса Borg — это использование метода __new__. Как мы знаем, метод __new__ отвечает за создание экземпляра объекта:
class Borg(object): _shared_state = {} def __new__(cls, *args, **kwargs): obj = super(Borg, cls).__new__(cls, *args, **kwargs) obj.__dict__ = cls._shared_state return obj
Начнем с краткого введения в метаклассы. Метакласс — это классы, экземпляры которых являются классами. С помощью метаклассов программисты получают возможность создавать классы своего собственного типа из предопределенных классов Python. Например, если у вас есть объект MyClass, вы можете создать метакласс MyKls, который переопределяет поведение MyClass так, как вам нужно.
Давайте разберемся с этим подробно.
Что было понятнее можно сказать что, метакласс это такая штука, которая создают объекты-классы. В Python все является объектом. Если мы пишем a = 5, тогда type(a) возвращает <type ‘int’>, что означает, что переменная a имеет тип int. Однако type(int) возвращает <type ‘type’>, что означает наличие метакласса, поскольку int является классом типа type.
Определение класса определяется его метаклассом, поэтому, когда мы хотим создать класс с помощью строки кода class A…, Python создает его с помощью A = type (name, base, dict), где:
• name: это название класса
• base: это базовый класс
• dict: это атрибуты класса
Теперь, если у класса есть предопределенный метакласс (по имени MetaKls), Python создает класс с помощью A = MetaKls(name, base, dict).
Рассмотрим пример реализации метакласса в Python 3.5:
class MyInt(type): def __call__(cls, *args, **kwds): print("***** Here's My int *****", args) print("Now do whatever you want with these objects...") return type.__call__(cls, *args, **kwds) class int(metaclass=MyInt): def __init__(self, x, y): self.x = x self.y = y i = int(4,5)
В итоге должно отобразиться что типа такого:
***** Here's My int ***** (4, 5) Now do whatever you want with these objects...
Специальный метод Python __call__ вызывается, когда необходимо создать объект для уже существующего класса. В этом коде, когда мы создаем экземпляр класса int с помощью int(4,5), вызывается метод __call__ метакласса MyInt, что означает, что метакласс теперь управляет созданием объекта.
Что то похожее что мы рассматривали раньше в шаблоне Singleton. Поскольку метакласс имеет больший контроль над созданием классов и созданием объектов, его можно использовать для создания синглетонов. Для управления созданием и инициализацией класса в метаклассах переопределяют методы __new__ и __init__.
Реализация Singleton с метклассами может быть лучше объяснена с помощью следующего примера кода:
class MetaSingleton(type): _instances = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super(MetaSingleton, cls).__call__(*args, **kwargs) return cls._instances[cls] class Logger(metaclass=MetaSingleton): pass logger1 = Logger() logger2 = Logger() print(logger1, logger2)
В качестве примера практического использования мы рассмотрим приложение базы данных. Рассмотрим пример облачной службы, которая включает в себя несколько операций чтения и записи в базе данных. Полный облачный сервис разбит на несколько сервисов, которые выполняют операции с базой данных.
Понятно, что общим ресурсом для разных сервисов является сама база данных. Итак, если нам нужно спроектировать облачный сервис, необходимо учесть следующие моменты:
Пример реализации:
import sqlite3 class MetaSingleton(type): _instances = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super(MetaSingleton, cls).__call__(*args, **kwargs) return cls._instances[cls] class Database(metaclass=MetaSingleton): connection = None def connect(self): if self.connection is None: self.connection = sqlite3.connect("db.sqlite3") self.cursorobj = self.connection.cursor() return self.cursorobj db1 = Database().connect() db2 = Database().connect() print ("Database Objects DB1", db1) print ("Database Objects DB2", db2)
В этом коде мы сделали следующее:
Если предположить, что вместо одного веб-приложения у нас есть кластерная установка с несколькими веб-приложениями, но только с одной БД. То это не очень хорошая ситуация для Singletons, потому что с каждым добавлением нового веб-приложения создается новый Singleton и добавляется новый объект, который будет запрашивать базу данных. Это приводит к несинхронизированным операциям с базой данных и потребует больше ресурсов. В таких случаях будет лучше использовать пул соединений с базой данных, чем использование простого Singletons.
Давайте рассмотрим другой сценарий, в котором мы внедряем службы проверки работоспособности (например, Nagios) для нашей инфраструктуры. Мы создаем класс HealthCheck, который реализован как Singleton. Мы также будем поддерживать список серверов, для которых должна выполняться проверка работоспособности. Если сервер удален из этого списка, программное обеспечение для проверки работоспособности должно обнаружить его и удалить с серверов, настроенных для проверки.
В следующем коде объекты hc1 и hc2 являются экземплярами класса в Singleton. Серверы добавляются в инфраструктуру для проверки работоспособности с помощью метода addServer(). В начале выполняется, итерация проверки работоспособности для этих серверов. Затем метод changeServer() удаляет последний сервер и добавляет новый. А затем, когда снова запускается проверка во второй итерации то используется уже измененный список серверов.
Все это делается с Singletons. Когда серверы добавляются или удаляются, проверка работоспособности должна быть таким объектом, который знает об изменениях, внесенных в инфраструктуру:
class HealthCheck: _instance = None def __new__(cls, *args, **kwargs): if not HealthCheck._instance: HealthCheck._instance = super(HealthCheck, \ cls).__new__(cls, *args, **kwargs) return HealthCheck._instance def __init__(self): self._servers = [] def addServer(self): self._servers.append("Server 1") self._servers.append("Server 2") self._servers.append("Server 3") self._servers.append("Server 4") def changeServer(self): self._servers.pop() self._servers.append("Server 5") hc1 = HealthCheck() hc2 = HealthCheck() hc1.addServer() print("Schedule health check for servers (1)..") for i in range(4): print("Checking ", hc1._servers[i]) hc2.changeServer() print("Schedule health check for servers (2)..") for i in range(4): print("Checking ", hc2._servers[i])
Примечание:
Этот пример не будет работать в Python 3.3 и более поздних версиях, если вы переопределяете и __new__, и __init__, вам следует избегать передачи дополнительных аргументов перезаписываемым объектным методам. Если вы переопределяете только один из этих методов, разрешается передавать дополнительные аргументы другому (поскольку это обычно происходит без вашей помощи).
Пример переопределения __new__ Python 3.
class Employee(object): def __new__(cls,*args, **kwargs): if not hasattr(cls,'_inst'): print(cls) cls._inst = super(Employee, cls).__new__(cls) # если запустить код написаный ниже в Python3, он выдаст ошибку «TypeError: object() takes no parameters» # cls._inst = super(Employee, cls).__new__(cls, *args,**kwargs) return cls._inst
Итак в результате работы кода с классом HealthCheck, должно отобразиться что то типа такого:
Schedule health check for servers (1).. Checking Server 1 Checking Server 2 Checking Server 3 Checking Server 4 Schedule health check for servers (2).. Checking Server 1 Checking Server 2 Checking Server 3 Checking Server 5
При работе с синглтонами нужно учитывать не только их достоинства но и их недостатки. Поскольку у Singleton есть глобальная точка доступа, при его использовать нужно всегда помнить о его основном недостатке. Singleton — это по сути это одно неделимое глобальное состояние. Это может быть как достоинство так и недостаток управляемости. Могут быть проблемы с тестированием, расширением, или детерминированность отдельных частей кода. Все компоненты, использующие этот паттерн, оказываются жестко связанными. Связанное зачастую приходится развязывать — например, чтобы написать тесты. И, тестировать код, в котором очень часто используются сингелтоны, может стать проблемой.
В этой статье вы узнали о шаблоне проектирования Singleton, о том что он используется, когда нужно иметь только один объект. Мы также рассмотрели различные способы реализации Singletons в Python. Классическая реализация допускает несколько попыток создания экземпляров, но всегда возвращает один и тот же объект. Также обсудили паттерн Monostate, который является вариацией паттерна Singleton. Monostate позволяет создавать несколько объектов, которые имеют одно и то же состояние.
Мы рассмотрели приложение, где Singleton может применяться для согласования операций с базами данных в нескольких сервисах.
Наконец, мы также рассмотрели главный недостаток Singletons.
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…
View Comments
Просто лучшая статья про Singleton что я видел, спасибо
Если во втором примере использования Singleton поменять пару строк местами и инициализировать второй объект ПОСЛЕ заполнения списка, то список оказывается пустым... Объясните? Иначе это уже не Singleton...
hc1 = HealthCheck()
hc1.addServer()
hc2 = HealthCheck()
Во время инициализации Второго объекта список серверов очищается, так как этот список один на все созданные объекты.
Да, тоже ломаю голову второй день. С singleton происходит следующее, по сути есть два патерна как создать одиночку - один через перегрузку __new__, другой через метакласс и перегрузку __call__. Но разница в том, что, кода возвращается уже созданный ранее _instance, то после вызова __new__ будет вызван __init__, а вот после вызова __call__ уже не будет вызван __init__. Так что смотрите сами, какое нужно поведение одиночки именно в вашей программе. На мой взгляд возможно 3 варианта: при попытке создать новую одиночку ругнуться исключением, либо выдать тот же экземпляр без запуска нового конструктора (через __call__), либо выдать тот же экземпляр, но уже с выполненным новым конструктором __init__ (что может вносить сложности в логику, если в __init__ уже создаются и настраиваются сложные вложенные объекты). Для демонстрации разницы между двумя последними подходами я написал небольшой наглядный листинг - https://pastebin.com/qnjkQht4
спасибо больше, за листинг! от души!!!
Подскажите пожалуйста, не могу понять, если в моём классе метод `__init__` ожидает аргумент, и я хочу сделать из класса Singleton,:
```
class Juggler:
def __new__(cls, *args, **kwargs):
if not hasattr(cls, 'instance'):
cls.instance = super(Juggler, cls).__new__(cls, *args, **kwargs)
return cls.instance
def __init__(self, queue: Queue) -> None:
self.queue = queue
```
получаю исключение
```
File "/opt/monapp2/app/juggler.py", line 77, in
Juggler(juggler_queue)
File "/opt/monapp2/app/juggler.py", line 32, in __new__
cls.instance = super(Juggler, cls).__new__(cls, *args, **kwargs)
TypeError: object() takes no parameters
```
Как правльно сделать?
В Python 3.3 и более поздних версиях, если вы переопределяете и __new__, и __init__, вам следует избегать передачи дополнительных аргументов перезаписываемым объектным методам. Если вы переопределяете только один из этих методов, разрешается передавать дополнительные аргументы другому (поскольку это обычно происходит без вашей помощи).
В данном случае используйте что то типа:
if not hasattr(cls, ‘instance’):
cls.instance = super(Juggler, cls).__new__(cls)
Спасибо за вопрос, внесу примечание в статью.
Да, так работает. Спасибо. Правда я не понял, как это происходит.
И огромное спасибо за статью, пожалуй, лучшая про Singleton, что я смог найти.
В новых питонах >= 3.3 нельзя передавать в __new__ ничего, кроме cls. Но при этом питон так работает, что за __new__ при возврате инстанса, созданного как результат __new__, будет вызван __init__, а в нём уже будут автоматически переданы *args и **kwargs, которые мы указываем при переопределении __new__ в своём классе, вот ссылка на пример двух подходов к созданию одиночки с передачей аргументов https://pastebin.com/qnjkQht4
Статья хорошая, правда местами перевод подводит. И всё же про отличие python >= 3.3 стоит написать перед первым примером, либо же не акцентироваться на этом и уже использовать современную версию т к на 3.2 и ранее уже никто сто лет не пишет, понятно, что оригинал статьи просто уже немного морально устарел. Ну и про нюанс с повторным выполнением __init__ при вызове __new__ стоит пояснить отдельно
Большое спасибо за расширенные комментарии.
"Отложенный экземпляр в Singleton" - только у меня не работает?
s = Singleton() # class initialized, but object not created
print(f's = {s}')
s1 = Singleton() # class initialized, but object not created
print(f's1 = {s1}')
s2 = Singleton.get_instance()
print(f's2 = {s2}')
s3 = Singleton() # instance already created
print(f's3 = {s3}')
s4 = Singleton() # instance already created
print(f's4 = {s4}')
Выдает разные экземпляры:
__init__ method called..
s =
__init__ method called..
s1 =
__init__ method called..
s2 =
Instance already created:
s3 =
Instance already created:
s4 =
Ваш классический синглтон не работает:
class CookieStorage(object):
def __new__(cls):
if not hasattr(cls, 'instance'):
cls.instance = super(CookieStorage, cls).__new__(cls)
return cls.instance
def __init__(self):
self.storage = {}
print('init')
def get(self):
return self.storage
def set(self, key, value):
self.storage[key] = value
def update(self, d):
self.storage.update(d)
s = CookieStorage()
s.set('key1', 'val1')
print("Object created", s, s.storage)
s1 = CookieStorage()
print("Object created", s1, s1.storage)
Вывод:
init
Object created {'key1': 'val1'}
init
Object created {}
да я жопу порвал пока не нашел ваше решение, спасибо! теперь большой массив данных без проблем переваривается