一尘不染

在Python中创建单例

python

这个问题不是为了讨论是否需要单例设计模式,是否是反模式,还是针对任何宗教战争,而是要讨论如何以最pythonic的方式在Python中最好地实现此模式。在这种情况下,我定义“最pythonic”表示它遵循“最小惊讶原则”。

我有多个将成为单例的类(我的用例用于记录器,但这并不重要)。当我可以简单地继承或修饰时,我不希望增加gumph来使几个类杂乱无章。

最佳方法:

方法1:装饰器

def singleton(class_):
    instances = {}
    def getinstance(*args, **kwargs):
        if class_ not in instances:
            instances[class_] = class_(*args, **kwargs)
        return instances[class_]
    return getinstance

@singleton
class MyClass(BaseClass):
    pass

优点

  • 装饰器的添加方式通常比多重继承更直观。

缺点

  • 使用MyClass()创建的对象将是真正的单例对象,而MyClass本身是一个函数,而不是类,因此你不能从中调用类方法。也为m = MyClass(); n = MyClass(); o = type(n)();随后m == n && m != o && n != o

方法2:一个基类

class Singleton(object):
    _instance = None
    def __new__(class_, *args, **kwargs):
        if not isinstance(class_._instance, class_):
            class_._instance = object.__new__(class_, *args, **kwargs)
        return class_._instance

class MyClass(Singleton, BaseClass):
    pass

优点

  • 这是一个真正的课堂

缺点

  • 多重继承-好!__new__从第二个基类继承期间可能被覆盖?人们必须思考的超出了必要。

方法3:元类

class Singleton(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]

#Python2
class MyClass(BaseClass):
    __metaclass__ = Singleton

#Python3
class MyClass(BaseClass, metaclass=Singleton):
    pass

优点
- 这是一个真正的课堂
- 自动神奇地涵盖继承
- 利用__metaclass__它的正确用途(使我意识到这一点)

缺点
有吗

方法4:装饰器返回具有相同名称的类

def singleton(class_):
    class class_w(class_):
        _instance = None
        def __new__(class_, *args, **kwargs):
            if class_w._instance is None:
                class_w._instance = super(class_w,
                                    class_).__new__(class_,
                                                    *args,
                                                    **kwargs)
                class_w._instance._sealed = False
            return class_w._instance
        def __init__(self, *args, **kwargs):
            if self._sealed:
                return
            super(class_w, self).__init__(*args, **kwargs)
            self._sealed = True
    class_w.__name__ = class_.__name__
    return class_w

@singleton
class MyClass(BaseClass):
    pass

优点

这是一个真正的课堂
自动神奇地涵盖继承

缺点

  • 创建每个新类没有开销吗?在这里,我们为希望创建单例的每个类创建两个类。就我而言,这很好,但我担心这可能无法扩展。当然,要扩展这种模式是否太容易是有争议的……
  • _sealed属性的重点是什么
  • 无法使用调用基类上同名的方法,super()因为它们会递归。这意味着你无法自定义__new__,也无法将需要调用的类作为子类__init__

方法5:一个模块

模块文件 singleton.py

优点

  • 简单胜于复杂
    缺点

阅读 444

收藏
2020-02-09

共1个答案

一尘不染

使用元类

我建议使用方法2,但最好使用元类而不是基类。这是一个示例实现:

class Singleton(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]

class Logger(object):
    __metaclass__ = Singleton

或在Python3中

class Logger(metaclass=Singleton):
    pass

如果要在__init__每次调用类时运行,请添加

        else:
            cls._instances[cls].__init__(*args, **kwargs)

对中的if陈述Singleton.__call__

关于元类的几句话。元类是类的类 ; 也就是说,类是其元类的实例。你可以使用来找到Python中对象的元类type(obj)。普通的新式类是类型type。Logger上面代码中的将会是type class ‘your_module.Singleton’,就像的(唯一的)实例Logger将是type一样class ‘your_module.Logger’。当你调用记录仪与Logger(),Python的首先要求的元类Logger,Singleton,做什么,允许实例创建要捷足先登。此过程与Python在通过class __getattr__引用类的一个属性时通过调用类要求做什么一样myclass.attribute

元类从本质上决定了类定义的含义以及如何实现该定义。参见例如http://code.activestate.com/recipes/498149/,它实际上是struct使用元类在Python中重新创建C风格的。线程元类的一些(具体)用例是什么?还提供了一些示例,它们通常似乎与声明性编程有关,尤其是在ORM中使用。

在这种情况下,如果你使用方法2,并且子类定义了一个__new__方法,则每次调用时都会执行SubClassOfSingleton()该方法-因为它负责调用返回存储实例的方法。对于元类,仅在创建唯一实例时才调用一次。你想自定义调用类的含义,该类由类的类型决定。

通常,使用元类实现单例是有意义的。单例很特别,因为它只能创建一次,而元类是自定义类创建的方式。如果需要以其他方式自定义单例类定义,则使用元类可以为你提供更多控制。

你的单例不需要多重继承(因为元类不是基类),但是对于使用多重继承的已创建类的子类,你需要确保单例类是第一个/最左边的一个具有重新定义的元类的类。__call__这不太可能成为问题。实例dict 不在实例的名称空间中,因此不会意外覆盖它。

你还将听到单例模式违反了“单一责任原则”-每个类只能做一件事。这样,你无需担心如果需要更改另一代码,便会弄乱代码要做的一件事,因为它们是分开的和封装的。元类实现通过了此测试。元类负责执行模式,创建的类和子类不必知道它们是单例。正如你在“ MyClass本身是一个函数而不是一个类,因此你无法从中调用类方法”中指出的那样,方法#1未能通过该测试。

Python 2和3兼容版本
编写适用于Python2和3的东西需要使用稍微复杂一些的方案。由于元类通常是type的子类type,因此可以在运行时使用它作为元类动态地创建一个中间基类,然后将其用作公共Singleton基类的基类。如下所示,解释起来比做起来难。

# works in Python 2 & 3
class _Singleton(type):
    """ A metaclass that creates a Singleton base class when called. """
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(_Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]

class Singleton(_Singleton('SingletonMeta', (object,), {})): pass

class Logger(Singleton):
    pass

具有讽刺意味的是,此方法使用子类来实现元类。一个可能的优点是,与纯元类不同,isinstance(inst, Singleton)它将返回True。

更正

在另一个主题上,你可能已经注意到了这一点,但是原始文章中的基类实现是错误的。_instances需要在类上引用,你需要使用super()或递归,并且__new__实际上是一个静态方法,你必须将该类传递给,而不是类方法,因为尚未在其上创建实际的类叫做。所有这些事情对于元类实现也是正确的。

class Singleton(object):
  _instances = {}
  def __new__(class_, *args, **kwargs):
    if class_ not in class_._instances:
        class_._instances[class_] = super(Singleton, class_).__new__(class_, *args, **kwargs)
    return class_._instances[class_]

class MyClass(Singleton):
  pass

c = MyClass()

室内设计师返校

我本来是在写评论,但是太长了,因此我将在此处添加。方法4比其他装饰器版本更好,但是它的代码比单例所需的代码更多,并且不清楚它的功能。

主要问题源于该类是它自己的基类。首先,让一个类成为几乎相同的类的子类不是很奇怪__class__吗?这也意味着你不能定义调用同名的方法对它们的基类的任何方法用super(),因为他们会重复。这意味着你的类无法自定义__new__,并且不能从需要对其__init__调用的任何类派生。

何时使用单例模式

你的用例是想要使用单例的更好示例之一。你在其中一项评论中说:“对我而言,伐木一直是Singletons的自然选择。” 你说的对。

人们说单身人士不好时,最常见的原因是他们是隐性的共享状态。尽管全局变量和顶级模块导入是显式共享状态,但通常会实例化传递的其他对象。这是一个好点,但有两个例外。

第一个,并且在各个地方都被提及的,是单例是恒定的。全局常数(尤其是枚举)的使用已被广泛接受,并且被认为是理智的,因为无论如何,任何用户都无法为其他任何用户弄乱它们。对于恒定的单例也同样如此。

第二个例外,与之相反,是相反的情况-当单例只是一个数据接收器,而不是数据源(直接或间接)时。这就是为什么记录器觉得单例的“自然”用法。由于各种用户没有以其他用户关心的方式更改记录器,因此并没有真正的共享状态。这消除了反对单例模式的主要观点,并使其成为合理的选择,因为它们易于执行任务。

这是来自http://googletesting.blogspot.com/2008/08/root-cause-of-singletons.html的报价:

现在,有一种Singleton可以。那是所有可达对象都是不可变的单例。如果所有对象都是不可变的,则Singleton没有全局状态,因为一切都是恒定的。但是将这种单身人士变成易变的人是如此容易,这是很滑的坡度。因此,我也反对这些Singleton,不是因为它们不好,而是因为它们很容易变坏。(作为一个附带说明,Java枚举就是这些单例。只要你不将状态放入枚举中就可以,所以请不要这样做。)

另一种半可接受的单例是那些不影响代码执行的单例,它们没有“副作用”。日志记录就是一个很好的例子。它加载了Singletons和全局状态。这是可以接受的(因为它不会对你造成伤害),因为无论是否启用给定的记录器,你的应用程序的行为都没有任何不同。此处的信息以一种方式流动:从你的应用程序进入记录器。甚至认为记录器是全局状态,因为没有信息从记录器流入你的应用程序,所以记录器是可以接受的。如果你想让测试断言某些东西正在被记录,那么你仍然应该注入记录器,但是总的来说,记录器即使处于满状态也不会有害。

2020-02-09