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

不禁感叹 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 差不多,但暴露的主要对象已经从类变成了实例,这个类已经不允许做实例化的操作了。这就是从代码层面控制了暴露的接口。