Frost's Blog
1300 字
7 分钟
再也别问 Singleton 了好吗?
2025-03-05

起笔的原因是群里的一段聊天:

20250305103123

不禁感叹 Singleton(单例模式)作为一个经典的设计模式,是如何被滥用的,特别是在 Python 这门语言中。它竟然成了一个八股式的面试题,就像「茴字有几种写法」一样,一直被问个没完。但我敢说,绝大多数人回答的时候,都是照本宣科,他们参考的网上的答案,也很少有能讲正确的。

先说结论

  • 在 Python 中,你不需要 Singleton。
  • 如果需要,就用模块级别的变量。

至于原因,让我们来看看几种流行的 Singleton 实现方式:

1. 装饰器#

def singleton(cls):
    instances = {}
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

# Usage
@singleton
class MyClass:
    ...

如无特殊说明,代码均由 Copilot 提供

这个方案的问题很明显:应用装饰器后改变了对象的类型,由一个 class 变成了一个 function。假使有人要用 isinstance(obj, MyClass) 来判断对象类型,就会报错。

这又涉及另一个问题,你的代码将被如何使用,取决于你暴露了什么,上面的例子中暴露的就是 MyClass 这个对象,那就要考虑会不会被当成 class 来用,以及若被这样使用,是不是合理的要求。

2. 类变量#

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

这个方案的问题是,如果有多个单例类型,就得写多个这样的类,而且这个类也不能被继承,继承之后,实际上还是共享同一个 _instance 变量,产生冲突,这不是我们想要的。还是老问题,你暴露了一个类,别人就会用类的方式来用。

第二个问题,这个方案没有屏蔽 __init__ 的调用,实际上你如果多次实例化 Singleton 类,虽然返回的对象 id 唯一,__init__ 方法还是会被调用多次,这可能不是你想要的。

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

    def __init__(self):
        print('init called')

s1 = Singleton()
s2 = Singleton()
print(s1 is s2)

# Output
# init called
# init called
# True

3. 继承#

class Singleton:
    _instances = {}

    def __new__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__new__(cls, *args, **kwargs)
        return cls._instances[cls]

# Usage
class MyClass(Singleton):
    ...

class MyAnotherClass(Singleton):
    ...

这个方案暴露的对象其实就是一个基类,它的标准用法就是让你用来继承的,它解决了上个方案的第一个问题,但第二个问题依然存在。

4. 元类#

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]

# Usage
class MyClass(metaclass=Singleton):
    ...

这个方案用了元类,就是所有致力于掌握 Python 高级语言特性的人们会想到的终级方案。它在元类级别直接拦截了实例化的调用,而不仅仅是 __new__ 方法,因为实例化的调用包括了 __new____init__,这样就解决了上面提到的第二个问题。如果你再运行上面的测试代码,会发现输出符合期待了:

s1 = MyClass()
s2 = MyClass()
print(s1 is s2)

# Output
# init called
# True

很完美,不是吗?先别沾沾自喜,看下面的代码:

class MyClass(metaclass=Singleton):
    def __init__(self, name: str) -> None:
        self.name = name

s1 = MyClass('Alice')
s2 = MyClass('Bob')
print(s1.name, s2.name)
# Output
# Alice Alice

你认为这个输出符合编写者的意图吗?不能说是也不能说不是,只是值得商榷,这取决于调用者如何看待单例模式。一个考虑是如果用方案 2 和 3, name 属性的值就会被统一改成 Bob,这本身已经体现了问题所在。你可能会说没人在单例类的实例化中传参,这点我也不确定,但我认为,有人这么做,是因为你暴露的接口允许他这么做。

5. 模块级别的变量#

class Singleton:
    ...

singleton = Singleton()

# Usage
from singleton_module import singleton

朴实无华,没有花里胡哨的东西,用这个答案如何能体现我精通 Python 呢?相反,我认为这个方案是最能实现原始需求的,相对来说问题最少,也最容易理解。就像人生的三重境界一样,最终还是要对花哨的东西袪魅,回到需求本身上去。这个方案里暴露的对象是唯一的 singleton 模块变量,你不可能用类的方式来用它,也不可能传参给它,这才是我们想要的。你甚至可以把类名写成 _Singleton 断了人的念想(当然这不是硬禁止,你想用还能用,别在这上面抬杠了)。

那如果,我还是想暴露 Singleton 类来做一些比如 isinstance 的操作呢?我承认有这个需求,那我们稍加改造一下:

class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is not None:
            raise TypeError("Singleton class cannot be instantiated twice")

        cls._instance = super().__new__(cls)
        return cls._instance

singleton = Singleton()

看上去好像跟方案 2 差不多,但暴露的主要对象已经从类变成了实例,这个类已经不允许做实例化的操作了。这就是从代码层面控制了暴露的接口。

再也别问 Singleton 了好吗?
https://frostming.com/2025/singleton/
作者
Frost Ming
发布于
2025-03-05
许可协议
CC BY-NC-SA 4.0