Реализация шаблона Singleton в Python

Spread the love

Шаблон Singleton (Одиночка) один из самых часто используемых шаблонов. Его можно встретить во множестве проектов и он относительно прост для обучения. Его обязательно нужно знать и уметь его использовать.

Статья написана на основе книги: Python: Master the Art of Design Patterns (Dusty Phillips, Chetan Giridhar, Sakis Kasampalis)

В этой статье будет кратко рассмотрены следующие темы:
• Как устроен Singleton
• Реальный пример паттерна Singleton
• Реализация шаблона Singleton в Python
• Шаблон Monostate

Как устроен Singleton

Шаблон Singleton предоставляет механизм создания одного и только один экземпляра объекта, и предоставление к нему глобальную точку доступа. Поэтому, Singletons обычно используются в таких случаях, как ведение журнала или операции с базой данных, диспетчера очереди печати и многих других, где существует необходимость иметь только один экземпляр, который доступен во всем приложении, чтобы избежать конфликтующих запросов на один и тот же ресурс. Например, мы можем захотеть использовать один объект базы данных для выполнения операций с БД для обеспечения согласованности данных или один объект класса ведения журнала для нескольких служб, чтобы последовательно выгружать сообщения журнала в определенный файл журнала.

Вкратце, цель шаблона Singleton заключаются в следующем:
• Обеспечение создания одного и только одного объекта класса
• Предоставление точки доступа для объекта, который является глобальным для программы
• Контроль одновременного доступа к ресурсам, которые являются общими

Простой способ реализации Singleton — сделать закрытым метод конструктора и создать статический метод, который выполняет инициализацию объекта. Таким образом, один объект создается при первом вызове, а класс будет всегда возвращать тот же объект при попытки новой инициализации объекта.
В Python мы реализуем эту идею по-другому, поскольку в нем нет возможности создавать приватные конструкторы.

Реализация классического синглтона в Python

Вот пример кода шаблона Singleton в Python v3.5. В этом примере мы делаем две основные вещи:

  1. Мы сделаем возможным создание только одного экземпляра класса Singleton.
  2. Если экземпляр существует, мы всегда будем использовать уже существующий объект.
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

Одним из вариантов использования шаблона 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

Singleton на уровне модуля

Все модули по умолчанию являются синглетонами из-за особенностей работы импорта в Python. Python работает следующим образом:

  1. Проверяет, был ли уже импортирован модуль.
  2. При импорте возвращает объект модуля. Если объекта не существует, то есть модуль не импортирован, он импортируется и создается его экземпляр.
  3. Когда модуль импортируется, он инициализируется. Но когда тот же модуль импортируется снова, он уже не инициализируется, что похоже на поведение Singleton, имеющим только один объект и возвращающим один и тот же объект.

Моностатический синглтон

В описание шаблона 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)

Первый пример использования Singleton

В качестве примера практического использования мы рассмотрим приложение базы данных. Рассмотрим пример облачной службы, которая включает в себя несколько операций чтения и записи в базе данных. Полный облачный сервис разбит на несколько сервисов, которые выполняют операции с базой данных.

Понятно, что общим ресурсом для разных сервисов является сама база данных. Итак, если нам нужно спроектировать облачный сервис, необходимо учесть следующие моменты:

  • Согласованность операций в базе данных — одна операция не должна приводить к конфликтам с другими операциями
  • Использование памяти и ЦП должно быть оптимальным для обработки нескольких операций с базой данных.

Пример реализации:

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)

В этом коде мы сделали следующее:

  1. Мы создали метакласс по имени MetaSingleton. Как мы объясняли в предыдущем разделе, специальный метод __call__ используется в метаклассе для создания Singleton.
  2. Класс Database создан с помощью метакласса MetaSingleton и является синглтон. Таким образом, когда создается экземпляр класса Database, он создает только один объект.
  3. Когда веб-приложение хочет выполнить определенные операции с БД, оно несколько раз создает экземпляр класса Database (в качестве примера, например в разных частях приложения), но создается только один объект. Поскольку существует только один объект, обращения к базе данных синхронизируются. Кроме того, такой подход позволяет ограничить использование системных ресурсов, и мы можем избежать ситуации нехватки памяти или ресурсов процессора.

Если предположить, что вместо одного веб-приложения у нас есть кластерная установка с несколькими веб-приложениями, но только с одной БД. То это не очень хорошая ситуация для Singletons, потому что с каждым добавлением нового веб-приложения создается новый Singleton и добавляется новый объект, который будет запрашивать базу данных. Это приводит к несинхронизированным операциям с базой данных и потребует больше ресурсов. В таких случаях будет лучше использовать пул соединений с базой данных, чем использование простого Singletons.

Второй пример использования Singleton

Давайте рассмотрим другой сценарий, в котором мы внедряем службы проверки работоспособности (например, 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 — это по сути это одно неделимое глобальное состояние. Это может быть как достоинство так и недостаток управляемости. Могут быть проблемы с тестированием, расширением, или детерминированность отдельных частей кода. Все компоненты, использующие этот паттерн, оказываются жестко связанными. Связанное зачастую приходится развязывать — например, чтобы написать тесты. И, тестировать код, в котором очень часто используются сингелтоны, может стать проблемой.

Резюме

В этой статье вы узнали о шаблоне проектирования Singleton, о том что он используется, когда нужно иметь только один объект. Мы также рассмотрели различные способы реализации Singletons в Python. Классическая реализация допускает несколько попыток создания экземпляров, но всегда возвращает один и тот же объект. Также обсудили паттерн Monostate, который является вариацией паттерна Singleton. Monostate позволяет создавать несколько объектов, которые имеют одно и то же состояние.
Мы рассмотрели приложение, где Singleton может применяться для согласования операций с базами данных в нескольких сервисах.
Наконец, мы также рассмотрели главный недостаток Singletons.

Была ли вам полезна эта статья?
[46 / 4.9]

Spread the love
Подписаться
Уведомление о
guest
14 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Илья
Илья
5 лет назад

Просто лучшая статья про Singleton что я видел, спасибо

Иван
Иван
5 лет назад

Если во втором примере использования Singleton поменять пару строк местами и инициализировать второй объект ПОСЛЕ заполнения списка, то список оказывается пустым… Объясните? Иначе это уже не Singleton…

hc1 = HealthCheck()
hc1.addServer()
hc2 = HealthCheck()

viking_unet
4 лет назад
Reply to  Иван

Да, тоже ломаю голову второй день. С singleton происходит следующее, по сути есть два патерна как создать одиночку — один через перегрузку __new__, другой через метакласс и перегрузку __call__. Но разница в том, что, кода возвращается уже созданный ранее _instance, то после вызова __new__ будет вызван __init__, а вот после вызова __call__ уже не будет вызван __init__. Так что смотрите сами, какое нужно поведение одиночки именно в вашей программе. На мой взгляд возможно 3 варианта: при попытке создать новую одиночку ругнуться исключением, либо выдать тот же экземпляр без запуска нового конструктора (через __call__), либо выдать тот же экземпляр, но уже с выполненным новым конструктором __init__ (что может вносить сложности в логику, если в __init__ уже создаются и настраиваются сложные вложенные объекты). Для демонстрации разницы между двумя последними подходами я написал небольшой наглядный листинг — https://pastebin.com/qnjkQht4

Sergo
Sergo
1 год назад
Reply to  viking_unet

спасибо больше, за листинг! от души!!!

Kirill Pletnev
Kirill Pletnev
5 лет назад

Подскажите пожалуйста, не могу понять, если в моём классе метод `__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
«`

Как правльно сделать?

edteam
Администратор
5 лет назад
Reply to  Kirill Pletnev

В Python 3.3 и более поздних версиях, если вы переопределяете и __new__, и __init__, вам следует избегать передачи дополнительных аргументов перезаписываемым объектным методам. Если вы переопределяете только один из этих методов, разрешается передавать дополнительные аргументы другому (поскольку это обычно происходит без вашей помощи).
В данном случае используйте что то типа:

if not hasattr(cls, ‘instance’):
cls.instance = super(Juggler, cls).__new__(cls)

Спасибо за вопрос, внесу примечание в статью.

Kirill Pletnev
Kirill Pletnev
5 лет назад

Да, так работает. Спасибо. Правда я не понял, как это происходит.

И огромное спасибо за статью, пожалуй, лучшая про Singleton, что я смог найти.

viking_unet
4 лет назад
Reply to  Kirill Pletnev

В новых питонах >= 3.3 нельзя передавать в __new__ ничего, кроме cls. Но при этом питон так работает, что за __new__ при возврате инстанса, созданного как результат __new__, будет вызван __init__, а в нём уже будут автоматически переданы *args и **kwargs, которые мы указываем при переопределении __new__ в своём классе, вот ссылка на пример двух подходов к созданию одиночки с передачей аргументов https://pastebin.com/qnjkQht4

viking_unet
4 лет назад

Статья хорошая, правда местами перевод подводит. И всё же про отличие python >= 3.3 стоит написать перед первым примером, либо же не акцентироваться на этом и уже использовать современную версию т к на 3.2 и ранее уже никто сто лет не пишет, понятно, что оригинал статьи просто уже немного морально устарел. Ну и про нюанс с повторным выполнением __init__ при вызове __new__ стоит пояснить отдельно

edteam
Администратор
4 лет назад
Reply to  viking_unet

Большое спасибо за расширенные комментарии.

Vladimir
Vladimir
4 лет назад

«Отложенный экземпляр в 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 =

Fg
Fg
4 лет назад

Ваш классический синглтон не работает:

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 {}

Oleh Kopyl
Oleh Kopyl
3 лет назад

да я жопу порвал пока не нашел ваше решение, спасибо! теперь большой массив данных без проблем переваривается