在 Python 中创建单例Python 2 和 3 兼容版本

这个问题不是为了讨论单例设计模式是否是可取的,是否是一种反模式,或者用于任何宗教战争,而是讨论如何以最pythonic的方式在Python中最好地实现这种模式。在这个例子中,我将“最pythonic”定义为它遵循“最小惊讶原则”。

我有多个类会变成单例(我的用例是记录器,但这并不重要)。我不希望在我可以简单地继承或装饰时用额外的口臭来混乱几个类。

最佳方法:


方法 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

    x = MyClass();
    y = MyClass();
    t = type(n)();
    

然后但是x == yx != t && y != t


方法 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

优点

  • 简单胜于复杂

缺点

  • 未延迟实例化

答案 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)

添加到 中的语句。ifSingleton.__call__

关于元类的几句话。元类是类的类;也就是说,类是其元类的实例。您可以使用 . 在 Python 中找到对象的元类。普通的新式类属于 类型 。 在上面的代码中将是类型,就像(唯一的)实例将是类型一样。当您用 调用 logger 时,Python 首先会询问 、 的元类,该做什么,从而允许实例创建被抢占。此过程与 Python 询问类在通过执行 引用其属性之一时调用要执行的操作相同。type(obj)typeLoggerclass 'your_module.Singleton'Loggerclass 'your_module.Logger'Logger()LoggerSingleton__getattr__myclass.attribute

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

在此情况下,如果您使用方法 #2,并且子类定义了一个方法,则每次调用时都会执行该方法,因为它负责调用返回存储实例的方法。对于元类,当创建唯一的实例时,它只会被调用一次。您希望自定义调用类的含义,该类由其类型决定。__new__SubClassOfSingleton()

通常,使用元类来实现单例是有意义的。单例是特殊的,因为只创建一次,元类是自定义类创建的方式。使用元类可以更好地控制,以防您需要以其他方式自定义单例类定义。

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

您还将听到单例模式违反了“单一责任原则” - 每个类应该只做一件事。这样,就不必担心在需要更改代码时搞砸代码所做的一件事,因为它们是分开的和封装的。元类实现通过此测试。元类负责强制执行模式,创建的类和子类不需要知道它们是单例方法 #1 未通过此测试,如您在“MyClass 本身是一个函数,而不是一个类,因此您无法从中调用类方法”中所述。

Python 2 和 3 兼容版本

编写在Python2和3中都有效的东西需要使用稍微复杂的方案。由于元类通常是类型的子类,因此可以使用元类在运行时动态创建一个中间基类,并将其用作其元类,然后将其用作公共基类的基类。这比做更难解释,如下图所示:typeSingleton

# 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

修正

在另一个主题上,您可能已经注意到了这一点,但是原始帖子中的基类实现是错误的。 需要在类上引用,你需要使用或递归,实际上是一个静态方法,你必须将类传递给它,而不是类方法,因为实际的类在被调用时还没有被创建。对于元类实现来说,所有这些事情都是正确的。_instancessuper()__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()

装饰师返回A类

我最初正在写评论,但它太长了,所以我会在这里添加这个。方法#4比其他装饰器版本更好,但它比单例所需的代码更多,并且不清楚它的作用。

主要问题源于该类是它自己的基类。首先,让一个类成为一个几乎相同的类的子类,具有相同的名称,只存在于其属性中,这难道不是很奇怪吗?这也意味着您无法定义任何在其基类上调用同名方法的方法,因为它们将递归。这意味着您的类无法自定义 ,并且无法从任何需要调用它们的类派生。__class__super()__new____init__

何时使用单例模式

您的用例是想要使用单例的更好示例之一。您在其中一条评论中说:“对我来说,日志记录似乎一直是单例的自然候选者。你是绝对正确的

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

第一个,也是在各个地方被提及的,是当单例是常数时。全局常量(尤其是枚举)的使用被广泛接受,并且被认为是合理的,因为无论如何,任何用户都不能为任何其他用户搞砸它们。对于常数单例也是如此。

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

以下是 http://googletesting.blogspot.com/2008/08/root-cause-of-singletons.html 的一句话:

现在,有一种单例是可以的。这是一个单例,其中所有可访问的对象都是不可变的。如果所有对象都是不可变的,则 Singleton 没有全局状态,因为一切都是恒定的。但是把这种单例变成可变的单例是如此容易,这是非常滑坡的。因此,我也反对这些单例,不是因为它们不好,而是因为它们很容易变坏。(作为旁注,Java枚举只是这些单例。只要你不把状态放到你的枚举中,你就可以了,所以请不要。

另一种半可接受的单例是那些不影响代码执行的单例,它们没有“副作用”。日志记录就是一个很好的例子。它加载了单例和全局状态。这是可以接受的(因为它不会伤害你),因为无论是否启用了给定的记录器,您的应用程序的行为都不会有任何不同。此处的信息以一种方式流动:从应用程序流入记录器。甚至思想记录器也是全局状态,因为没有信息从记录器流入您的应用程序,记录器是可以接受的。如果您希望测试断言某些内容正在被记录,您仍然应该注入记录器,但通常记录器尽管充满了状态,但并无害。


答案 2
class Foo(object):
     pass

some_global_variable = Foo()

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