小编典典

在 Python 中创建单例

all

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

我有多个将成为单例的类(我的用例是记录器,但这并不重要)。当我可以简单地继承或装饰时,我不希望用添加的口香糖来弄乱几个类。

最佳方法:


方法一:装饰器

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 == y但是x != t && y != t


方法二:基类

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__正确目的(并让我意识到)

缺点

  • 有吗?

方法四:装饰器返回同名类

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属性的重点是什么
  • 不能在基类上调用同名方法 usingsuper()因为它们会递归。这意味着您不能自定义__new__也不能子类化需要您调用的类__init__

方法5:一个模块

一个模块文件singleton.py

优点

  • 简单胜于复杂

缺点

  • 没有延迟实例化

阅读 103

收藏
2022-02-25

共1个答案

小编典典

使用元类

我会推荐 Method #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 typeLogger在上面的代码中将是 type class 'your_module.Singleton',就像(唯一)实例Logger将是 type class 'your_module.Logger'。当你用 调用 logger 时Logger(),Python 首先询问 ,
的元类要做什么LoggerSingleton从而允许实例创建被抢占​​。这个过程与 Python
询问一个类__getattr__当你通过调用它的一个属性时调用它来做什么是一样的myclass.attribute

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

在这种情况下,如果您使用 Method #2* ,并且子类定义了一个方法,那么 每次
调用时__new__都会执行该方法——因为它负责调用返回存储实例的方法。使用元类,它只会在创建唯一实例时 被调用一次。 您想 自定义调用
class 的含义
,这由它的类型决定。
SubClassOfSingleton() *

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

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

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

Python 2 和 3 兼容版本

编写适用于 Python2 和 3 的东西需要使用稍微复杂的方案。由于元类通常是
typetype的子类,因此可以使用一个在运行时动态创建一个中间基类,并将其作为元类,然后将
用作公共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
就没有全局状态,因为一切都是不变的。但是很容易把这种单例变成可变单例,很滑坡。因此,我也反对这些
Singletons,不是因为它们不好,而是因为它们很容易变坏。(作为旁注,Java 枚举就是这类单例。只要您不将状态放入枚举中就可以了,所以请不要。)

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

2022-02-25