在Python中创建单例模式

1655
这个问题并不讨论单例设计模式是否可取,是否是反模式或任何宗教战争,而是讨论如何以最符合Python风格的方式实现此模式。在这种情况下,我将“最符合Python风格”定义为遵循“最小惊奇原则”。
我有多个类需要成为单例(我的用例是一个记录器,但这不重要)。我不希望在几个类中添加额外的内容,而是可以简单地继承或装饰。
最佳方法:

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

优点

  • 装饰器的特性通常比多重继承更加直观。

缺点

  • While objects created using MyClass() would be true singleton objects, MyClass itself is a function, not a class, so you cannot call class methods from it. Also for

    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__?这需要比必要更多的思考。

方法三:元类

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__的类。

方法五:一个模块

一个名为singleton.py的模块文件。

优点

  • 简单胜于复杂。

缺点

  • 不是延迟实例化的。

20
另外三种技术:使用模块替代(通常情况下,我认为这是Python中更适合的模式,但这也取决于你用它做什么);创建一个单一实例并直接操作它(foo.x 或者如果你坚持可以用 Foo.x 替代 Foo().x);使用类属性和静态/类方法(Foo.x)。 - Chris Morgan
23
如果你只打算使用类方法或静态方法,那么实际上就不需要创建一个类了。 - Cat Plus Plus
5
@猫: 效果类似,但创建全局变量的原因可能是任何事情,包括不知道更好的方法。为什么要创建一个单例?如果你不知道答案,那就不应该在这里。这种明确性不仅更符合Python的风格,而且使维护更加简单。是的,单例是全局变量的语法糖,但类也是一堆难看东西的语法糖,没有人会告诉你总是最好不用它们。 - theheadofabroom
35
反单例情绪是最糟糕的模拟船运动编程。对于那些听过(很少有人真正阅读过)“Goto语句被认为是有害的”并认为无论上下文如何,goto语句都是糟糕代码的标志的人也是如此。 - Hejazzman
5
你好,感谢你详细的帖子。我对模式编程和Python都比较新,而且我很惊讶尽管方法2似乎是最为人熟知的一种(它无处不在),但几乎没有人提到每次使用Singleton()或MyClass()时,尽管仅创建了一个对象,init()也会被调用。我没有尝试过,但据我所知,其他方法也是如此。 当实现单例模式时,这似乎并不理想,难道我漏掉了什么吗?当然,解决方案是设置属性以避免两次执行__init__。只是好奇。 - chrisvp
显示剩余30条评论
42个回答

6

以下是我对单例模式的实现。你只需要在类前加上装饰器,然后就可以使用 Instance 方法获得单例。下面是一个示例:

@Singleton
class Foo:
    def __init__(self):
        print 'Foo created'

f = Foo() # Error, this isn't how you get the instance of a singleton

f = Foo.Instance() # Good. Being explicit is in line with the Python Zen
g = Foo.Instance() # Returns already created instance

print f is g # True

以下是代码:

class Singleton:
    """
    A non-thread-safe helper class to ease implementing singletons.
    This should be used as a decorator -- not a metaclass -- to the
    class that should be a singleton.

    The decorated class can define one `__init__` function that
    takes only the `self` argument. Other than that, there are
    no restrictions that apply to the decorated class.
 
    To get the singleton instance, use the `Instance` method. Trying
    to use `__call__` will result in a `TypeError` being raised.

    Limitations: The decorated class cannot be inherited from.

    """

    def __init__(self, decorated):
        self._decorated = decorated

    def Instance(self):
        """
        Returns the singleton instance. Upon its first call, it creates a
        new instance of the decorated class and calls its `__init__` method.
        On all subsequent calls, the already created instance is returned.

        """
        try:
            return self._instance
        except AttributeError:
            self._instance = self._decorated()
            return self._instance

    def __call__(self):
        raise TypeError('Singletons must be accessed through `Instance()`.')

    def __instancecheck__(self, inst):
        return isinstance(inst, self._decorated)

3
这不是真正的单例模式。在真正的单例模式下,当用SingletonList = Singleton(list).Instance(); print(SingletonList is type(SingletonList)())来输出时,应该打印出True;而使用你的代码时输出了False。(链接为示例代码) - GingerPlusPlus
1
@GingerPlusPlus,我知道有一些限制,但你指出的这个我不知道。谢谢你提醒我。不幸的是,我现在没有时间考虑解决方案。 - Paul Manta
根据@GingerPlusPlus的评论,我给这个答案打了一个负一分。如果你修复了它,请告诉我,我会删除这个负一分。 - Ben Kovitz

6

使用类变量(无装饰器)

通过覆盖 __new__ 方法来返回同一个类的实例。使用布尔值只为第一次初始化类:

class SingletonClass:
    _instance = None

    def __new__(cls, *args, **kwargs):
        # If no instance of class already exits
        if cls._instance is None:
            cls._instance = object.__new__(cls)
            cls._instance._initialized = False
        return cls._instance
        
    def __init__(self, *args, **kwargs):
        if self._initialized:
            return

        self.attr1 = args[0]
        # set the attribute to `True` to not initialize again
        self._initialized = True

6
我会推荐一种使用元类的优雅解决方案。
class Singleton(type): 
    # Inherit from "type" in order to gain access to method __call__
    def __init__(self, *args, **kwargs):
        self.__instance = None # Create a variable to store the object reference
        super().__init__(*args, **kwargs)

    def __call__(self, *args, **kwargs):
        if self.__instance is None:
            # if the object has not already been created
            self.__instance = super().__call__(*args, **kwargs) # Call the __init__ method of the subclass (Spam) and save the reference
            return self.__instance
        else:
            # if object (Spam) reference already exists; return it
            return self.__instance

class Spam(metaclass=Singleton):
    def __init__(self, x):
        print('Creating Spam')
        self.x = x


if __name__ == '__main__':
    spam = Spam(100)
    spam2 = Spam(200)

输出:

Creating Spam

从输出结果可以看出,只实例化了一个对象。


5
from functools import cache

@cache
class xxx:
   ....

非常简单易行,且有效!


2
小心 - 只有在实例化时不传递任何参数才有效。如果你这样做了,它就会缓存未命中,并且你最终会得到多个实例。 - chrisRedwine

3

我来贡献一下我的想法。这是一个简单的装饰器。

from abc import ABC

def singleton(real_cls):

    class SingletonFactory(ABC):

        instance = None

        def __new__(cls, *args, **kwargs):
            if not cls.instance:
                cls.instance = real_cls(*args, **kwargs)
            return cls.instance

    SingletonFactory.register(real_cls)
    return SingletonFactory

# Usage
@singleton
class YourClass:
    ...  # Your normal implementation, no special requirements.

我认为它相对于其他解决方案具有以下优点:

  • 它清晰简洁(在我看来 ;D)。
  • 它的作用完全封装。您不需要改变YourClass的任何实现,这包括不需要为类使用元类(请注意,上面的元类是在工厂中而不是“真实”类中)。
  • 它不依赖于任何猴子补丁。
  • 对于调用者而言,它是透明的:
    • 调用者仍然只需导入YourClass,它看起来像一个类(因为它确实是一个类),并且以正常方式使用它。无需使调用者适应工厂函数。
    • YourClass() 实例化的内容仍然是您实现的 YourClass 的真实实例,而不是任何代理,因此没有机会产生副作用。
    • isinstance(instance, YourClass) 和类似操作仍像预期那样工作(尽管这部分需要abc,因此排除 Python <2.6)。

我想到了一个缺点:无法通过隐藏它的工厂类透明地调用真实类的classmethods和staticmethods。我很少使用这个需求,所以从未遇到过它,但可以通过在工厂上使用自定义元类来实现__getattr __() 来委托所有-ish属性访问到真实类。

我实际上发现了一个相关的模式更有用(并不是说这些东西经常需要), 它是一种“Unique”模式,其中使用相同参数实例化类会导致获取相同的实例。即“每个参数单例”。以上对此进行了很好的适应,并变得更加简洁:

def unique(real_cls):

    class UniqueFactory(ABC):

        @functools.lru_cache(None)  # Handy for 3.2+, but use any memoization decorator you like
        def __new__(cls, *args, **kwargs):
            return real_cls(*args, **kwargs)

    UniqueFactory.register(real_cls)
    return UniqueFactory

尽管如此,我同意一般建议,如果您认为需要这些东西之一,那么您应该停下来问自己是否真的需要。99%的情况下,YAGNI。


3

第三种方法看起来非常整洁,但是如果您希望程序在Python 2Python 3中运行,则无法正常工作。即使通过测试保护不同的变体也会失败,因为Python 3版本在Python 2中会出现语法错误。

感谢Mike Watkins:http://mikewatkins.ca/2008/11/29/python-2-and-3-metaclasses/。如果您想让程序在Python 2和Python 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]

MC = Singleton('MC', (object), {})

class MyClass(MC):
    pass    # Code for the class implementation

我猜测在赋值语句中,“object”需要替换为“BaseClass”,但我还没有尝试过(我已经尝试了代码,如所示)。

肯定这不是元类 - 在Python3中,要使用元类构造MyClass,您需要执行class MyClass(metaclass=Singleton) - theheadofabroom
mikewatkins.ca链接已(有效地)损坏。 - Peter Mortensen

3

也许我对单例模式的理解有误,但我的解决方案非常简单和实用(Python化?)。这段代码实现了两个目标:

  1. 使得Foo类的实例在任何地方都可以使用(全局)。
  2. Foo类只能存在一个实例。

这是代码:

#!/usr/bin/env python3

class Foo:
    me = None

    def __init__(self):
        if Foo.me != None:
            raise Exception('Instance of Foo still exists!')

        Foo.me = self


if __name__ == '__main__':
    Foo()
    Foo()

输出

Traceback (most recent call last):
  File "./x.py", line 15, in <module>
    Foo()
  File "./x.py", line 8, in __init__
    raise Exception('Instance of Foo still exists!')
Exception: Instance of Foo still exists!

2

这与fab的答案略有相似,但并非完全相同。

单例模式不要求我们能够多次调用构造函数。由于单例只应该被创建一次,那么它不应该被看作被创建了多次吗?“欺骗”构造函数可能会影响可读性。

因此,我的建议只是这样:

class Elvis():
    def __init__(self):
        if hasattr(self.__class__, 'instance'):
            raise Exception()
        self.__class__.instance = self
        # initialisation code...

    @staticmethod
    def the():
        if hasattr(Elvis, 'instance'):
            return Elvis.instance
        return Elvis()

这并不排除用户代码使用构造函数或字段 instance 的可能性:

if Elvis() is King.instance:

如果您确定Elvis尚未创建,而King已经创建,则可以使用以下代码:

if (!isset($elvis) && isset($king)) {
    // do something
}

但是更好的做法是鼓励用户普遍使用the方法:

Elvis.the().leave(Building.the())

为了使其完整,您还可以覆盖__delattr__(),如果尝试删除instance,则引发异常,并覆盖__del__(),使其引发异常(除非我们知道程序正在结束...)。

更多改进


感谢那些在评论和编辑方面提供帮助的人,欢迎提供更多。虽然我使用Jython,但这应该更普遍,并且线程安全。
try:
    # This is jython-specific
    from synchronize import make_synchronized
except ImportError:
    # This should work across different python implementations
    def make_synchronized(func):
        import threading
        func.__lock__ = threading.Lock()
    
        def synced_func(*args, **kws):
            with func.__lock__:
                return func(*args, **kws)

        return synced_func

class Elvis(object): # NB must be subclass of object to use __new__
    instance = None

    @classmethod
    @make_synchronized
    def __new__(cls, *args, **kwargs):
        if cls.instance is not None:
            raise Exception()
        cls.instance = object.__new__(cls, *args, **kwargs)
        return cls.instance
    
    def __init__(self):
        pass
        # initialisation code...

    @classmethod
    @make_synchronized
    def the(cls):
        if cls.instance is not None:
            return cls.instance
        return cls()

注意事项:

  1. 如果在Python2.x中不从object子类化,则会得到旧式类,它不使用__new__
  2. 当装饰__new__时,必须使用@classmethod或__new__将是未绑定的实例方法
  3. 通过使用元类来改进可能会更好,因为这将允许您使the成为类级别属性,可能将其重命名为instance

虽然这是单例模式的一个稍微不同的解释,但我相信它仍然有效,尽管我可能会倾向于使用__new__而不是__init__,因为它纯粹作用于类属性,这可以防止出现第二个实例。那么,这与方法2之间的区别在于,尝试初始化多次是返回单个实例还是引发异常。我认为我很满意任何一种满足单例模式的方式,其中一种更容易使用,而另一种则更明确地表明它是单例。 - theheadofabroom
显然,在__init__中使用类名会防止子类化,但尽管这使事情变得更容易,但并非必需。 - theheadofabroom
谢谢...啊,是的,在异常被抛出之前有一个短暂的第二个实例。我已经修改了__init__,希望这样可以进行子类化... - mike rodent
哎呀,还得调整一下你的“the”版本,以反映类字段的使用。 - mike rodent
2017-12-23 踩一下... 请礼貌地添加评论来解释您的想法:这可能对我有帮助,或者您可能会发现学到了什么。 - mike rodent
显示剩余3条评论

2

这个答案可能不是你想要的。我想要一个单例,意思是只有该对象拥有其身份,可以进行比较。在我的情况下,它被用作哨兵值。对此的答案非常简单,使任何对象mything = object(),由于Python的本质,只有那个对象才会拥有其身份。

#!python
MyNone = object()  # The singleton

for item in my_list:
    if item is MyNone:  # An Example identity comparison
        raise StopIteration

我了解到模块实际上可以被多次导入,在这种情况下,它只是一个本地单例,其在任何情况下都不是真正的单例。 - ThorSummoner
你能详细说明一个模块如何被多次导入吗?我见过的唯一情况是当模块加载时发生异常,用户可能会稍后再次加载该模块,但副作用已经发生,因此某些操作可能会执行第二次。 - sleblanc
一旦一个模块完全被加载,我并没有看到其他让这个模块再次运行的方法,除非通过使用 evalimportlib.reload 强制解释器执行它。 - sleblanc
@sleblanc 我以为我在这个话题上有一个SO帖子,但找不到了;这是一个顶级的谷歌结果:https://dev59.com/PljUa4cB1Zd3GeqPT7QL#55910681。如果我没记错的话,我需要这个来猴子补丁一些错误行为在Python的stdlib的SSL证书链信任断言时使用多个域名的特定方式,允许一些模块有他们的SSL接口替换为猴子补丁版本和其他人不,能够根据需要交换它们。我不建议猴子补丁,但我很高兴有这个选项存在 :) - ThorSummoner

2

我无意中制作了一个简单的东西,想分享一下...

class MySingleton(object):
    def __init__(self, *, props={}):
        self.__dict__ = props

mything = MySingleton()
mything.test = 1
mything2 = MySingleton()
print(mything2.test)
mything2.test = 5
print(mything.test)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接