有没有一种简单、优雅的方式来定义单例模式?

527

在Python中,似乎有许多定义单例的方法。在Stack Overflow上是否有共识意见?


9
Singletons are Pathological Liars,它们不是病态的说谎者吗? - Jonas Byström
21
这个问题不适合我们的问答格式 - 我认为这不是一个主观性的问题,有没有一种方法可以提出这样的问题以使其符合 Stack Overflow 的问答格式? - binithb
18
我不同意这不是有建设性的。如果转移到http://programmers.stackexchange.com,它能重新开放吗? - langlauf.io
1
@stackoverflowwww 不行,因为这是基于个人观点的,而progs.SE不喜欢这样的内容。 - ratchet freak
1
@ratchetfreak 这个问题受欢迎的原因是像我这样的人正在寻找在Python中创建单例的不同方法。有一些替代方案,各有优缺点,或者只适用于特定情况。这个问题可以重新表述为“ Python中存在哪些不同的方式来创建单例?我特别关注基于类和基于类实例的解决方案之间的差异。” - langlauf.io
显示剩余7条评论
21个回答

431

我并不觉得有必要使用单例模式,因为只包含函数(而非类)的模块可以很好地充当单例。所有变量都将绑定到模块,无论如何也无法重复实例化。

如果您确实希望使用类,则在Python中没有创建私有类或私有构造函数的方法,因此除了通过API的惯例使用保护措施外,无法防止多次实例化。我仍然会将方法放在一个模块中,并将该模块视为单例。


14
构造函数不能只是检查实例是否已经被创建,如果已经创建则抛出异常吗? - Casebash
53
只要你的设计不需要使用继承,那么这是没问题的。如果需要继承,那么下面大部分的答案更加适合。 - Jim Jeffries
12
当出现循环导入时,代码会出现问题。 - dieend
16
如果我想让这个模块可继承,我应该怎么做? - yossi
17
在我看来,这是错误的。关于模块级别接口的一个烦恼是管理导入。例如,Python的“logging”是一个模块级别接口。为了确保您在“logging”之后完全清理,必须调用“logging.shutdown()”。这意味着您必须将“logging”导入调用“shutdown”的模块中。如果它是单例模式,则可以在传递给任何模块的实例上调用关闭。 - Matt
显示剩余17条评论

371

这是我自己实现的单例模式。您只需要给类添加装饰器即可,要获取单例,您需要使用 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. Also, the decorated class cannot be
    inherited from. 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.

    """

    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)

34
Python自带电池,因此应该将其作为“设计模式”标准��的一部分,谢谢。 - dashesy
15
@akhan,我故意决定不支持带参数的构造函数,因为这些参数只会在第一次使用时被使用,之后都将被忽略。这会使你的代码非常难以跟踪,因为你可能会在不同的地方使用不同的参数,但你可能不知道哪个调用实际上是初始化单例的那一个。 - Paul Manta
8
如果你真的想要用参数初始化你的单例模式,你应该有一个单独的 initialize() 方法,可以接受任何参数,并在第二次调用时抛出异常。 - Paul Manta
42
这是一个非常糟糕的单例模式实现。首先,它不是一个合适的装饰器,因为它没有使用functools.wrapsfunctools.update_wrapper。其次,必须通过调用Foo.Instance()来获取实例,这是非常不符合Python风格的。而且完全可以像这样实现Foo()。第三,替换类会产生意外的结果,比如type(Foo.instance()) is Foo -> False - Aran-Fey
13
@Aran-Fey看起来这个解决方案真的让你失望了。我不认为Paul Manta曾经说过这是全世界最好的解决方案。他只是想回答原作者的问题。我认为这是对Python中存在的问题的一个很好的解决方案。 - JokerMartini
显示剩余10条评论

222

您可以通过以下方法覆盖 __new__ 方法:

class Singleton(object):
    _instance = None
    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super(Singleton, cls).__new__(
                                cls, *args, **kwargs)
        return cls._instance


if __name__ == '__main__':
    s1 = Singleton()
    s2 = Singleton()
    if (id(s1) == id(s2)):
        print "Same"
    else:
        print "Different"

71
警告:如果__new__()方法返回类的一个实例,那么新实例的__init__()方法将被调用,形式为__init__(self[, ...]),其中self是新实例,其余参数与传递给__new__()方法的相同。 如果任何Singleton的子类实现了__init__()方法,它将会多次使用相同的self进行调用。我最终使用了工厂模式代替。 - alsuren
7
这里建议使用元类来改进,具体答案参见此链接:https://dev59.com/7nVD5IYBdhLWcg3wRpeX#33201 - underrun
2
这会产生以下警告 - singleton.py:9: DeprecationWarning: object.__new__() takes no parameterscls._instance = super(Singleton,cls).__new__(cls,*args,**kwargs) - Siddhant
2
@Siddhant:更糟糕的是,在Python 3中,该警告变成了一个错误。请参阅http://bugs.python.org/issue1683368和http://blog.jaraco.com/2014/05/how-to-safely-override-init-or-new-in.html获取更多详细信息。 - Jason R. Coombs
另一个问题是对这个实现进行子类化。如果在之前已经实例化过,子类将包含对超类的引用 _instance(但反之则不然)。为了解决这个问题,您可以将 if not cls._instance 替换为 if type(cls._instance) != cls(这样做是因为 None 不是 Singleton,而 Singleton 不是 SubSingleton(Singleton))。 - rlat
截至Python 3.10,我测试了这段代码,没有出现任何错误。 - Scott

133

在Python中实现单例的一种稍微不同的方法是borg模式,由Alex Martelli(Google员工和Python天才)提出。

class Borg:
    __shared_state = {}
    def __init__(self):
        self.__dict__ = self.__shared_state

所以,与其强制所有实例具有相同的标识,它们共享状态。


108
也被称为单态模式。可能比单例模式更加邪恶。 - Tom Hawtin - tackline
6
无法与新式类一起使用。 - James Emerton
27
为什么这个在新式类中不起作用,有人能解释一下吗? - Stephen Emslie
7
我刚试了一下Python 2.7.2,使用新样式类可以正常工作。 - voithos
9
你说得对,它并不是单例模式,这也可能是为什么Alex Martelli给它取了一个不同的名字的部分原因,但它的效果非常相似。 - martineau
显示剩余8条评论

93

模块化方法效果良好。如果我绝对需要一个单例,我更喜欢元类的方法。

class Singleton(type):
    def __init__(cls, name, bases, dict):
        super(Singleton, cls).__init__(name, bases, dict)
        cls.instance = None 

    def __call__(cls,*args,**kw):
        if cls.instance is None:
            cls.instance = super(Singleton, cls).__call__(*args, **kw)
        return cls.instance

class MyClass(object):
    __metaclass__ = Singleton

2
这种模式违反了“单一职责原则”(http://c2.com/cgi/wiki?SingleResponsibilityPrinciple)。请参见http://blogs.msdn.com/scottdensmore/archive/2004/05/25/140827.aspx中的第(2)点。 - haridsv
22
@haridsv 我不同意。事实上,类是单例模式这一事实被在元类实现中抽象出来了——类本身并不知道也不关心它是单例的,因为它不负责强制要求那个要求,元类才是。如你所指出的,下面的方法显然是违规的。而基类方法则处于介于两者之间的状态。 - agf
2
@dare2be:您提到的复制问题是否可以通过让元类为创建的类添加__deepcopy__()方法来解决? - martineau
3
@martineau:这是覆盖的type.__init__,而不是MyClass.__init__ - Eric
2
另一个stackoverflow评论提到,您可以通过覆盖__new__()来修复此错误class SingletonMeta(type): def __new__(cls, name, bases, dict): dict['__deepcopy__'] = dict['__copy__'] = lambda self, *args: self return super(SingletonMeta, cls).__new__(cls, name, bases, dict)
  • https://dev59.com/uWLVa4cB1Zd3GeqPsQAM#9887928
- James McGuigan
显示剩余4条评论

50

看看这个实现来自PEP318,使用装饰器实现单例模式:

def singleton(cls):
    instances = {}
    def getinstance():
        if cls not in instances:
            instances[cls] = cls()
        return instances[cls]
    return getinstance

@singleton
class MyClass:
    ...

25
这个装饰器的问题在于'MyClass'不再是一个类,例如super()和classmethods等将无法使用:@singleton class MyClass(BaseClass): def __init__(self): super(MyClass, self).__init__() - mkm
1
似乎装饰器应该应用于__new__方法,而不是类本身,以处理继承问题。这时,装饰器的优雅可读性就会降低。或者,装饰器需要与其装饰的类进行交互,使__new__函数能够做出有意义的事情。 - F1Rumors

46

Python文档有涉及到这个问题:

class Singleton(object):
    def __new__(cls, *args, **kwds):
        it = cls.__dict__.get("__it__")
        if it is not None:
            return it
        cls.__it__ = it = object.__new__(cls)
        it.init(*args, **kwds)
        return it
    def init(self, *args, **kwds):
        pass

我可能会将它重写为更像这样的形式:

class Singleton(object):
    """Use to create a singleton"""
    def __new__(cls, *args, **kwds):
        """
        >>> s = Singleton()
        >>> p = Singleton()
        >>> id(s) == id(p)
        True
        """
        it_id = "__it__"
        # getattr will dip into base classes, so __dict__ must be used
        it = cls.__dict__.get(it_id, None)
        if it is not None:
            return it
        it = object.__new__(cls)
        setattr(cls, it_id, it)
        it.init(*args, **kwds)
        return it

    def init(self, *args, **kwds):
        pass


class A(Singleton):
    pass


class B(Singleton):
    pass


class C(A):
    pass


assert A() is A()
assert B() is B()
assert C() is C()
assert A() is not B()
assert C() is not B()
assert C() is not A()

扩展这个应该相对清晰简单:

class Bus(Singleton):
    def init(self, label=None, *args, **kwds):
        self.label = label
        self.channels = [Channel("system"), Channel("app")]
        ...

12
因为您是唯一提及Guido van Rossum实现的人,所以给您点赞。然而,您自己的版本是错误的:在__new__中不应使用hasattrgetattr,因为它们都调用object.__getattribute__,后者通过整个类层次结构查找您的"__self__"属性,而不仅仅是当前类。如果Guido使用__dict__进行属性访问,那肯定有他的原因。试试:class A(GuidoSingleton): pass, class B(A): pass, class C(YourSingleton): pass, class D(C): pass, print(A(), B(), C(), D())。所有子类都引用同一个YourSingleton实例! - Géry Ogam
7
感谢您提醒我们,Python文档始终是寻找单例和其他设计模式的最佳起点。+1 - user-asterix
这个线程安全吗? - Jemshit Iskenderov
只有在编写了额外的代码以使其线程安全时,@JemshitIskenderov 才能使用它。这个特定的实现防止创建同一类的多个对象。然而,除非所创建的对象被传递(有几种不同的方法可以跨线程传递对象),否则它不能跨线程防止这种情况。 - Brian Bruggeman
我遇到了一个错误:TypeError: __init __()仅接受1个位置参数,但提供了4个。唯一不同的是使用了dataclasses。 - Zhanwen Chen

29

我非常不确定,但我的项目使用“惯例单例”(而不是强制单例),也就是说,如果我有一个名为DataController的类,我会在同一模块中定义它:

_data_controller = None
def GetDataController():
    global _data_controller
    if _data_controller is None:
        _data_controller = DataController()
    return _data_controller

虽然这不是很优雅,因为它有完整的六行。但我的所有单例都使用这种模式,它至少非常明确(这是Pythonic的)。


在Python中,一切都应该遵循约定(因为通常可以绕过强制边界)。个人而言,我更喜欢使用classmethod和class variable来访问和存储实例,这样就不必使用“global”。(虽然我通常不鼓励使用“global”,但这是少数几种可以接受的用例之一。) - schlamar
DataController 应该改为 _DataController 吗?否则可以直接实例化它。 - nos
这是我认为最好的解决方案,因为当你在未来遇到这段代码时,它是最容易理解的。 - joanis

28
正如被接受的答案所说,最惯用的方法就是使用一个模块。
考虑到这一点,这里有一个概念验证:
def singleton(cls):
    obj = cls()
    # Always return the same object
    cls.__new__ = staticmethod(lambda cls: obj)
    # Disable __init__
    try:
        del cls.__init__
    except AttributeError:
        pass
    return cls

请查看Python数据模型,以获取有关__new__的更多详细信息。

示例:

@singleton
class Duck(object):
    pass

if Duck() is Duck():
    print "It works!"
else:
    print "It doesn't work!"

注意事项:

  1. 你必须使用新式类(从object派生)来实现此功能。

  2. 单例在定义时就被初始化,而不是在第一次使用时。

  3. 这只是一个玩具示例。我从未在生产代码中使用过它,并且也没有计划这样做。


我尝试过这个,但是出现了错误:TypeError: unbound method <lambda>() must be called with Integer instance as first argument (got type instance instead)我的Integer类就是你的Duck类:@singleton class Integer(object): """ 用于整数类型对象的类 """ pass - Tom Prats
谢谢指出。我不知道为什么会发生这种情况,但编辑后的版本应该可以在Python 2.7和3.3上运行。 - Lambda Fairy
这不太好,当类被定义时就调用了“init()”方法(虽然您可能希望等到第一次使用它之前再调用),并且在每次调用“Duck()”时都会调用它。 - tiho
2
我已经记录了第一个问题,并修复了第二个。感谢您指出。 - Lambda Fairy

18

我曾经在Python中编写单例时使用了一个类,其中所有成员函数都带有classmethod修饰符。

class Foo:
    x = 1
  
    @classmethod
    def increment(cls, y=1):
        cls.x += y

我喜欢这种方法,但有一个小问题。至少在Python 2.6中,你不能将__len____getitem__等方法作为类方法使用,因此你没有像使用对象那样灵活自定义的能力。由于我经常想将单例用作数据集合,所以这有点令人失望。 - Dan Homerick
在我看来,这不过是将一堆东西封装到一个命名空间中...并没有什么问题,有些人甚至认为这是一个非常好的想法(import this)——只是这种方法不过是简单明了,并且似乎非常接近使用全局变量,而这通常被认为是一种糟糕的工程实践。 - martineau
2
@martineau,我建议使用单例模式实现与使用全局变量非常接近。 - David Locke
2
单例模式比全局变量更好的两个方面是:它们完全不会污染全局命名空间(或者像你的答案一样少污染),并且它们还提供了延迟评估,而全局变量通常不提供这种功能(你的答案也不提供)。 - martineau
1
对于__len____getitem__甚至@property,您可以使用__metaclass__设置为定义上述内容的类。这很有效。我建议将类设置为单例模式,因为它是元类的实例,符合语言设计。实际上,所有方法都可以在元类中定义,然后类将仅用作对单例的引用。 - Leonid Usov

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