单例模式的主要目的是保证在系统中,某个类只能有一个实例存在。比如保存系统基本配置信息的类,在很多地方都要用到,没有必要频繁创建实例与销毁实例,只需要保存一个全局的实例对象即可,这样可以减少对内存资源的占用。
Python模块实现单例
Python 的模块就是天然的单例模式,因为模块在第一次导入时,会生成 .pyc
文件,当第二次导入时,就会直接加载 .pyc
文件,而不会再次执行模块代码。因此,我们只需把相关的函数和数据定义在一个模块中,就可以获得一个单例对象了。如果我们真的想要一个单例类,可以考虑这样做:
class Singleton(object): |
将上面的代码保存在文件 mysingleton.py
中,要使用时,直接在其他文件中导入此文件中的对象,这个对象即是单例模式的对象
from mysingleton import singleton |
使用装饰器实现单例
def singleton(cls): |
使用装饰器实现的单例模式在多线程环境下可能会引发竞争条件(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() # 线程锁
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
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()要点
- 双重检查*(Double-Checked Locking):
- 先检查 _instance 是否存在(不加锁)。
- 进入锁定区间后,再次检查 _instance 是否已创建。
- 这样可避免多个线程重复创建实例。
- 使用 with _lock 语法确保锁在作用域结束后自动释放。
使用类实现单例
class Singleton(object): |
和使用装饰器的方法一样,使用类的单例模式在多线程环境下会失效。
解决方法也和“使用装饰器的方法”一样,加锁。
【推荐】__new__
实现单例
__new__
方法在实例创建前被调用,可确保只创建一个实例:
class Singleton: |
和使用装饰器的方法一样,使用new的单例模式在多线程环境下会失效。
解决方法也和“使用装饰器的方法”一样。(加锁,Double-Check)
【推荐】metaclass实现单例
在Python中,对象是类的实例,而类是元类的实例。在没有指定的情况下,type是所有类的元类。
自定义类的元类,可以拦截类的创建过程,这是metaclass实现单例模式的基础。
这里不懂的话,需要仔细看一下Python元类的知识~
|
难点解释:
- 当调用
Singleton()
时,实际上调用的是SingletonMeta
的__call__
方法。 cls
是Singleton
类,而不是SingletonMeta
元类- 元类属性会自动成为类的属性,所以
cls._instances
指向了SingletonMeta._instances super().__call__(*args, **kwargs)
的等价操作type.__call__(cls, *args, **kwargs)
- 而
type.__call__
会调用类的__new__
和__init__
方法,从而创建Singleton的实例对象。
使用 functools.lru_cache
lru_cache
可以缓存实例,从而实现单例:
from functools import lru_cache |