Python单例模式总结

单例模式的主要目的是保证在系统中,某个类只能有一个实例存在。比如保存系统基本配置信息的类,在很多地方都要用到,没有必要频繁创建实例与销毁实例,只需要保存一个全局的实例对象即可,这样可以减少对内存资源的占用。

Python模块实现单例

Python 的模块就是天然的单例模式,因为模块在第一次导入时,会生成 .pyc 文件,当第二次导入时,就会直接加载 .pyc 文件,而不会再次执行模块代码。因此,我们只需把相关的函数和数据定义在一个模块中,就可以获得一个单例对象了。如果我们真的想要一个单例类,可以考虑这样做:

class Singleton(object):
def foo(self):
pass
singleton = Singleton()

将上面的代码保存在文件 mysingleton.py 中,要使用时,直接在其他文件中导入此文件中的对象,这个对象即是单例模式的对象

from mysingleton import singleton

使用装饰器实现单例

def singleton(cls):
_instance = {}

def _singleton(*args, **kwargs):
if cls not in _instance:
_instance[cls] = cls(*args, **kwargs)
return _instance[cls]
return _singleton

@singleton
class A(object):
a = 1
def __init__(self, x = 0):
self.x = x

a1 = A(1)
a2 = A(2)
print(a1 == a2)

使用装饰器实现的单例模式在多线程环境下可能会引发竞争条件(race condition),导致创建多个实例。因此,在并发访问的场景下,需要加锁来保证线程安全。


为什么需要加锁?

假设多个线程同时执行 _singleton() 方法:

if cls not in _instance:
_instance[cls] = cls(*args, **kwargs)
  • 线程 A 判断 _instance 中没有 cls,但此时 线程 B 可能也执行到此处。
  • 两个线程可能同时创建 cls(),最终 _instance 可能存储多个实例。

这种竞争条件会导致单例模式失效


如何加锁?

可以使用 threading.Lock() 来确保只有一个线程能创建实例:

import threading
from functools import wraps

def singleton(cls):
_instance = {}
_lock = threading.Lock() # 线程锁

@wraps(cls)
def inner(*args, **kwargs):
if cls not in _instance: # 第一次判断(非严格保证)
with _lock: # 进入锁定区间
if cls not in _instance: # 再次判断,避免多个线程重复创建
_instance[cls] = cls(*args, **kwargs)
return _instance[cls]

return inner

@singleton
class Cls:
def __init__(self, value=0):
self.value = value

# 测试多线程环境
import threading

def test_singleton():
obj = Cls()
print(id(obj))

threads = [threading.Thread(target=test_singleton) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()

要点

  1. 双重检查*(Double-Checked Locking):
    1. 先检查 _instance 是否存在(不加锁)。
    2. 进入锁定区间后,再次检查 _instance 是否已创建。
    3. 这样可避免多个线程重复创建实例。
  2. 使用 with _lock 语法确保锁在作用域结束后自动释放。

使用类实现单例

class Singleton(object):

def __init__(self):
pass

@classmethod
def instance(cls, *args, **kwargs):
if not hasattr(Singleton, "_instance"):
Singleton._instance = Singleton(*args, **kwargs)
return Singleton._instance

和使用装饰器的方法一样,使用类的单例模式在多线程环境下会失效。

解决方法也和“使用装饰器的方法”一样,加锁。

【推荐】__new__实现单例

__new__方法在实例创建前被调用,可确保只创建一个实例:

class Singleton:
_instance = None

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

# 测试
a = Singleton()
b = Singleton()
print(a is b) # True

和使用装饰器的方法一样,使用new的单例模式在多线程环境下会失效。

解决方法也和“使用装饰器的方法”一样。(加锁,Double-Check)

【推荐】metaclass实现单例

在Python中,对象是类的实例,而类是元类的实例。在没有指定的情况下,type是所有类的元类。

自定义类的元类,可以拦截类的创建过程,这是metaclass实现单例模式的基础。

这里不懂的话,需要仔细看一下Python元类的知识~


class SingletonMeta(type):
_instances = {}

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

class Singleton(metaclass=SingletonMeta):
pass

obj1 = Singleton()
obj2 = Singleton()

print(obj1 is obj2) # True,说明 obj1 和 obj2 是同一个实例

难点解释:

  1. 当调用 Singleton() 时,实际上调用的是 SingletonMeta__call__ 方法。
  2. clsSingleton 类,而不是 SingletonMeta 元类
  3. 元类属性会自动成为类的属性,所以cls._instances指向了SingletonMeta._instances
  4. super().__call__(*args, **kwargs)的等价操作type.__call__(cls, *args, **kwargs)
  5. type.__call__会调用类的__new____init__方法,从而创建Singleton的实例对象。

使用 functools.lru_cache

lru_cache 可以缓存实例,从而实现单例:

from functools import lru_cache

@lru_cache(maxsize=1)
def get_instance():
class Singleton:
pass
return Singleton()

# 测试
a = get_instance()
b = get_instance()
print(a is b) # True
文章作者: Met Guo
文章链接: https://guoyujian.github.io/2025/03/21/Python%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F%E6%80%BB%E7%BB%93/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Gmet's Blog