协慌网

登录 贡献 社区

在 Python 中创建单例

这个问题不是讨论单身人士设计模式是否可取,反模式,还是任何宗教战争,而是讨论如何以最蟒蛇的方式在 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

优点

  • 这是一个真正的课程

缺点

  • 多重继承 - eugh!在从第二个基类继承期间可能会覆盖__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__

答案

使用元类

我建议使用方法#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)

Singleton.__call__if语句。

关于元类的几句话。元类是类的类 ; 也就是说,类是其元类实例 。您可以在 Python 中找到type(obj)的对象的元类。普通的新式类是类型type 。上面代码中的Logger类型为class 'your_module.Singleton' ,就像Logger的(唯一)实例类型为class 'your_module.Logger'类型class 'your_module.Logger' 。当您使用Logger()调用 logger 时,Python 首先会询问LoggerSingleton的元类,该怎么做,允许抢占实例创建。这个过程与 Python 在通过执行myclass.attribute引用其中一个属性时通过调用__getattr__来要求类做什么相同。

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

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

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

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

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

Python 2 和 3 兼容版本

编写适用于 Python2 和 3 的东西需要使用稍微复杂的方案。由于元类通常是类型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__任何类派生它们。

何时使用单例模式

您的用例是想要使用单例的更好示例之一。你在其中一条评论中说:“对我而言,伐木似乎一直是单身人士的天生候选人。” 你说得

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

第一个,也就是各个地方提到的一个,就是单身人士不变的时候 。使用全局常量,特别是枚举,被广泛接受,并且被认为是理智的,因为无论如何, 没有任何用户可以将它们搞砸到任何其他用户 。对于常数单例也是如此。

第二个例外,提到的更少,恰恰相反 - 当单例只是数据接收器而不是数据源(直接或间接)时。这就是伐木工人对单身人士的 “自然” 用途的原因。由于各种用户没有以其他用户关心的方式更改记录器 ,因此没有真正的共享状态 。这否定了针对单例模式的主要论点,并且因为它们易于用于任务而使它们成为合理的选择。

以下是http://googletesting.blogspot.com/2008/08/root-cause-of-singletons.html的引用:

现在,有一种 Singleton 可以。这是一个单例,其中所有可到达的对象都是不可变的。如果所有对象都是不可变的,那么 Singleton 没有全局状态,因为一切都是常量。但是很容易将这种单体变成可变的单体,它是非常滑的斜率。因此,我也反对这些单身人士,不是因为他们不好,而是因为他们很容易变坏。 (作为旁注,Java 枚举只是这些单例。只要你没有将状态放入枚举中就可以了,所以请不要。)

另一种半单可接受的单身人士是那些不影响你的代码执行的人,他们没有 “副作用”。记录是完美的例子。它充满了单身人士和全球状态。这是可以接受的(因为它不会伤害你),因为无论是否启用给定的记录器,您的应用程序都不会有任何不同。这里的信息以一种方式流动:从您的应用程序到记录器。即使记录器是全局状态的,因为没有信息从记录器流入您的应用程序,因此记录器是可接受的。如果您希望测试断言某些内容已被记录,您仍应注入记录器,但一般情况下,尽管状态已满,但记录器无害。

class Foo(object):
     pass

some_global_variable = Foo()

模块只导入一次,其他一切都是过度思考。不要使用单身并尽量不使用全局变量。

使用模块。它只导入一次。在其中定义一些全局变量 - 它们将是单例的 '属性'。添加一些函数 - 单例的 '方法'。