在 Python 中创建单例

新手上路,请多包涵

_这个问题不是为了讨论 单例设计模式 是否可取、是否是反模式或任何宗教战争,而是讨论如何以最 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 属性有什么意义
  • 不能使用 super() 在基类上调用同名方法,因为它们会递归。这意味着您不能自定义 __new__ 并且不能子类化需要您调用到 __init__ 的类。

方法5:一个模块

一个模块文件 singleton.py

优点

  • 简单胜于复杂

缺点

  • 没有延迟实例化

原文由 theheadofabroom 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 484
2 个回答

使用元类

我会推荐 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)

到 --- 中的 Singleton.__call__ if 语句。

关于元类的几句话。元类是 类的类;也就是说,一个类是 它的元类的一个实例。您可以使用 type(obj) 在 Python 中找到对象的元类。普通的新式类是 type 类型。 Logger in the code above will be of type class 'your_module.Singleton' , just as the (only) instance of Logger will be of type class 'your_module.Logger' .当你用 Logger() 调用 logger 时,Python 首先询问 — 的元类, Logger Singleton ,做什么,允许实例创建被抢占。这个过程与 Python 通过调用 __getattr__ 询问类做什么是相同的,当您通过执行 myclass.attribute 引用它的一个属性时。

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

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

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

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

您还会听说单例模式违反了“单一职责原则”——每个类应该 只做一件事。这样一来,如果您需要更改另一件事,您就不必担心会弄乱代码所做的一件事,因为它们是独立的且被封装的。元类实现 通过了这个测试。元类负责 执行模式,创建的类和子类不需要 知道它们是单例方法 #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 就没有全局状态,因为一切都是不变的。但是很容易把这种单例变成可变单例,很滑坡。因此,我也反对这些 Singletons,不是因为它们不好,而是因为它们很容易变坏。 (作为旁注,Java 枚举就是这类单例。只要您不将状态放入枚举中就可以了,所以请不要。)

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

原文由 agf 发布,翻译遵循 CC BY-SA 4.0 许可协议

使用元类

我会推荐 方法 #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) 找到对象的元类。普通的新型类的类型是 typeLogger in the code above will be of type class 'your_module.Singleton' , just as the (only) instance of Logger will be of type class 'your_module.Logger' .当您使用 Logger() 调用记录器时,Python 首先询问 — 的元类, LoggerSingleton 的元类。这个过程与 Python 通过调用 __getattr__ 询问一个类做什么相同,当您通过执行 myclass.attribute 引用它的一个属性时。

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

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

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

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

您还会听说单例模式违反了“单一职责原则”——每个类应该 只做一件事。这样一来,如果您需要更改另一件事,您就不必担心弄乱代码所做的事情,因为它们是分开和封装的。元类实现 通过了这个测试。元类负责 执行模式,创建的类和子类不需要 知道它们是单例方法 #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__ 实际上 是一个静态方法 类 to ,而不是类方法,因为在调用它时 尚未创建 实际类。所有这些对于元类实现也是如此。

 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 就没有全局状态,因为一切都是不变的。但是把这种单例变成可变单例太容易了,这是一个很滑的斜坡。所以,我也反对这些Singletons,不是因为它们不好,而是因为它们很容易变坏。 (作为旁注,Java 枚举就是这类单例。只要您不将状态放入枚举中就可以,所以请不要这样做。)

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

原文由 agf 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题