вторник, 14 декабря 2010 г.

Python. Синглтоны.

Я занимаюсь написанием большой продуманной системой - фреймворком модульных приложений на пайтоне. Приходится использовать такие редкие в наше время паттерны проектирывания как синглтоны. Однако, пайтон не даёт готовых решений.
Но давайте сначала посмотрим как оно сделано в других языках.

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

UPD1: Реализация переписана на метаклассы. Смотри в самый низ статьи.
UPD2: Добавлен хинт для адресации к переменным сразу через dot: Foo.name (было: Foo().name).


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


a = "hello"
b = "goodbye"

def b.upcase
gsub(/(.)(.)/) { $1.upcase + $2 }
end

puts a.upcase #HELLO
puts b.upcase #GoOdBye

via Wyldrodney
Во второй строке создаётся сам объёкт, который будет синглтоном, в 4ой добавляется метод и объект становится синглтоном(только у нас в голове, конечно). В девятой строке демонстрируется как оно работает.
Красиво? Нет/иногда. Практично? Иногда. Синглтон ли это? Нет.

Однако, поддерживаются и классические одиночки, которые мы так любим в CPP, Java.

require 'singleton'
class Test
include Singleton
def message(msg)
puts msg
end
end

Test.instance.message('test')

via Yumitsu
Импортируем модуль, добавляем методы, обращаемся через получаемый член instance. Стоит обратить внимание на то, что класс уже инициализирован.

В пайтоне синглтонами тоже привыкли называть нечто иное. В "стеке" все ссылкаются на один и тот же вопрос, а точнее выбранный ответ.
Сама суть данного метода описана здесь.
Коротко говоря:
Синглтонами в пайтоне называют модули с глобальными переменными и функциями(без классов). Своё состояние они хранят в глобальных переменных.


inited = False

def init(files=None):
global inited
db = MimeTypes()

Инициализируем переменные, обращаемся к ним.

Однако любого программиста смутит данный способ, ровно как и способ из руби. В отличии от руби, в пайтоне нет стандартного модуля синглтонов. Поэтому, я решил что стоит сделать свой в составе библиотеки agatsuma.commons.

Для начала, следует определить что такое "хорошо", т.е. как мы хотим чтобы выглядело использование синглтонов.


class Foo(Singleton):
def __init__(self, arg):
self.arg = arg
print arg, 'inited!'
def test(self):
print self.arg
Foo(1).test() => 1 inited!\n1
Foo().test() => 1

Наследуем базовый тип, который делает всё что нужно. При вызове конструктора класса, первый раз вызывается __init__, остальные разы возвращается один и тот же инстанс, который был создан с самого начала.
В силу того, что пайтон поддерживает множественное наследование, это достаточно удобный вариант. Удобнее чем указание метакласса.

Ну давайте двигаться к цели!
Первое, что мог найти либой человек попробуя погуглить, это статья в вики и данный код:

class Singleton(object):
obj = None # Атрибут для хранения единственного экземпляра
def __new__(cls,*dt,**mp): # класса Singleton.
if cls.obj is None: # Если он еще не создан, то
cls.obj = object.__new__(cls,*dt,**mp) # вызовем __new__ родительского класса
return cls.obj # вернем синглтон

Суть: перегрузка метода __new__, который создаёт инстанс. Если инстанс есть - возвращаем его, нет - создаём.
Вот только есть проблема.
DeprecationWarning: object.__new__() takes no parameters
Что это значит, почему и как, было обсуждено в рассылке python-dev.
The message means just what it says. :-) There's no point in calling
object.__new__() with more than a class parameter, and any code that
did so was just dumping those args into a black hole.

The only time when it makes sense for object.__new__() to ignore extra
arguments is when it's not being overridden, but __init__ *is* being
overridden -- then you have a completely default __new__ and the
checking of constructor arguments is relegated to __init__.

The purpose of all this is to catch the error in a call like
object(42) which (again) passes an argument that is not used. This is
often a symptom of a bug in your program.

--Guido

В общем, вики требует правки.
Цель "object.__new__(cls,*dt,**mp)" - создать экземпляр класса передавая аргументы в его конструктор.
Стоит отметить особенность пайтона: Сначала вызывается __new__, а потом, с теми же аргументами, __init__. Нет нужды передавать аргументы в object.__new__.

Код становится такого вида:

class Singleton(object):
__instance = None
def __new__(cls, *a, **kwa):
if cls.__instance is None:
cls.__instance = object.__new__(cls)
return cls.__instance


Однако, из предыдущей особенности становится логично что вытекает ещё одна: не важно какой инстанс был возвращён в __new__, __init__ всё-равно будет вызван.
Я много думал о решении данной проблемы, но самое лёгкое - удаление __init__ из класса после выполнения инициализации.
По идее, надо бы удалять его сращу после выполнения, но это мы можем сделать только в __init__, но там что-то делать будет не так красиво.
Можно, конечно, сделать свой __init__ в синглтоне, который будет вызывать другой метод, который и будет самим конструктором.
Например:

class Singleton(object):
__instance = None
__inited = False
def __new__(cls, *a, **kwa):
if cls.__instance is None:
cls.__instance = object.__new__(cls)
return cls.__instance
def __init__(self, *a, **kwa):
if not self.__inited and hasattr(self, "_init"):
self._init(*a, **kwa)
self.__inited = True


Тогда использование будет таким:

class Foo(Singleton):
def _init(self, arg):
print arg


Но ведь мы хотим чтобы не было отличий от обычного класса, чтобы было красиво, так?
Последняя мысль, которая у меня появилась, это удалять __init__ при втором вызове. Собственно, вот и конечный класс синглтона.

nullfunc = lambda x: None

class Singleton(object):
__instance = None
__init_replaced = False
def __new__(cls, *a, **kwa):
if cls.__instance is None:
cls.__instance = object.__new__(cls)

elif not cls.__init_replaced:
cls.__init__ = nullfunc
cls.__init_replaced = True

return cls.__instance

Удалить __init__, увы, нельзя, но можно его заменить. Чтобы не менять его каждый раз, сделаем флаг(5).

Однако, данный вариант грязен, не так красив и вообще может кого-то смутить своим видом.

Есть и альтернативный путь - реализация через перегрузку метода __call__ метакласса синглтона.
Дело в том, что при каждом вызове(а точнее перед) конструктора класса, вызывается метод __call__ метакласса. Да! Это то что нам нужно. Не буду томить, вот готовый код:


class SingletonMeta(type):
def __init__(cls, name, bases, dict):
super(SingletonMeta, cls).__init__(name, bases, dict)
cls.instance = None
def __call__(self,*args,**kw):
if self.instance is None:
self.instance = super(SingletonMeta, self).__call__(*args, **kw)
return self.instance

class Singleton(object):
__metaclass__ = SingletonMeta


Метакласс можно использовать когда и где хочется, но в пайтоне нельзя делать несколько метаклассов сразу, поэтому удобнее использовать во многих случаях именно наследование класса.

Отдельно хочется добавить что сам метакласс был взят из поста в "стеке", за что автору спасибо.

И для самых капризных осталось добавить ещё 2 возможности:
1) Адресация к переменным через Foo.name вместо Foo().name
2) Вызов функций класса через Foo.name() вместо Foo().name()

Первое решается просто добавлением в мета-класс перегруженного метода __getattr__:

def __getattr__(cls, name):
return getattr(cls(), name)


Однако, второй пункт реализовать я пока не смог. Если у вас есть идеи - с удовольствием выслушаю.

Вот и всё. Есть вопросы? Задавайте здесь, в комментах.

Конечный вариант можно наблюдать в сырце из пакета agatsuma.commons.

6 комментариев:

  1. Этот комментарий был удален автором.

    ОтветитьУдалить
  2. class Singleton(object):
    instance = None
    inited = False
    def __new__(cls, *a, **kwa):
    if cls.instance is None:
    cls.instance = object.__new__(cls)
    return cls.instance

    def __init__(self):
    if not self.__class__.inited:
    self.__class__.inited = True
    self.init()


    def init(self):
    pass

    Наследуемся и используем вместо __init__, наш init. И радуемся

    ОтветитьУдалить
  3. class Singleton(object):
    __instance = None
    __inited = False
    def __new__(cls, *a, **kwa):
    if cls.__instance is None:
    cls.__instance = object.__new__(cls)
    return cls.__instance
    def __init__(self, *a, **kwa):
    if not self.__inited and hasattr(self, "_init"):
    self._init(*a, **kwa)
    self.__inited = True

    Этот код не будет работать. В частности вот эта строчка:
    if not self.__inited and hasattr(self, "_init"):
    __inited это атрибут класса а не экземпляра. self.__inited же ссылка на экземпляр.

    ОтветитьУдалить
  4. Этот комментарий был удален автором.

    ОтветитьУдалить
  5. Для чего в 8 примере __init_replaced?
    Почему нельзя сделать так?

    class Singleton(object):
    ....__instance = None
    ....def __new__(cls):
    ........if cls.__instance is None:
    ............cls.__instance = super(C, cls).__new__(cls)
    ........else:
    ............cls.__init__ = lambda x: None
    ........return cls.__instance

    ОтветитьУдалить
  6. Указанный синглтон в метаклассах ломается с помощью s1 = copy(s2)

    >>> s = Singleton()
    connected
    >>> s1 = Singleton()
    >>> s is s1
    True
    >>> from copy import copy
    >>> s2 = copy(s1)
    >>> s2 is s1
    False
    >>> s
    <__main__.Singleton object at 0x0437F110>
    >>> s1
    <__main__.Singleton object at 0x0437F110>
    >>> s2
    <__main__.Singleton object at 0x0437FB70>

    ОтветитьУдалить