单例模式一种常见的设计模式,并且也是最基础的设计模式,需要每个开发人员都熟练掌握。
当你希望在整个系统中,某个类只能出现一个实例时,就需要学会使用单例模式。
比较常见的场景是:某个项目的配置信息存放在一个配置文件中,通过一个 Config
的类来读取配置文件的信息。如果在程序运行期间,有很多地方都需要使用配置文件的内容,也就是说,很多地方都需要创建 Config
对象的实例,这就导致系统中存在多个 Config
的实例对象,而这样会严重浪费内存资源,尤其是在配置文件内容很多的情况下。事实上,类似 Config
这样的类,我们希望在程序运行期间只存在一个实例对象。
在 Python 中,可以用多种方法实现单例模式,常见的有:
- 通过使用模块
- 通过装饰器
- 通过__new__方法
- 通过元类
不同的方法,有不同的坑,对 Python 机制认识不够的同学,很容易踩到坑,本文用实例来详细说明一下这几种方法的区别,以及那些不容易被察觉的坑点,给出最佳的选择方案。
下面就详细说明以下这几种实现方法:
1. 通过使用模块
在Python中,Python 的模块就是天然的单例模式。
因为模块在第一次导入时,会生成 .pyc
文件,当第二次导入时,就会直接加载 .pyc
文件,而不会再次执行模块代码。因此,我们只需把相关的函数和数据定义在一个模块中,就可以获得一个单例对象了。
看如下示例:
# 例如在config.py文件中定义Config类和实例
class Config:
LOG_LEVEL = 'INFO'
...
config = Config()
# 然后在其他文件中,通过import导入此实例
from config import config
print(config.LOG_LEVEL)
# 输出结果:INFO
如上所示,通过在文件中先生成一个类实例,然后在别的文件中直接导入这个类实例,就实现了单例模式。
2. 通过装饰器
另外一种典型的用法是使用类装饰器,由于逻辑比较易懂,被广泛被开发人员使用。
如下是一个例子,在每次对 Config 实例化的时候,都会进入 Singleton 类装饰器,该装饰器维护一个 dict,key 为 cls,value 为 instance,每次实例化都检查该 cls 是否已经在 dict 中存在,若存在则直接将之前实例化的对象返回,如此来保证单例。
from functools import wraps
def Singleton(cls):
instance = {}
@wraps(cls)
def wrapper(*args, **kwargs):
if cls not in instance:
instance[cls] = cls(*args, **kwargs)
return instance[cls]
return wrapper
@Singleton
class Config(object):
pass
cfg1 = Config()
cfg2 = Config()
print(cfg1 == cfg2)
运行后,输出结果 True
不过,这种方法虽然逻辑非常清晰易懂,但却有一个非常大的问题:经过 Singleton 装饰的后,其返回的是函数,因为无法被继承。
装饰器由于本身机制的限制,在实例单例的同时也带来了一定的 “副作用”。
仔细一想,用装饰器实现单例的思路,不就是拦截实例对象的创建,来保证类只有唯一的实例嘛!
那我们只要换种方法,来拦截实例对象的创建不就行了。
通常的方法,有两种:
- new 方法
- 元类
3. 通过__new__方法
先说一下 new 方法,它是 Python 中的魔法方法之一。
很多人可能对他不是很熟悉,不过完全没有关系,你只要知道,当你在实例化的时候,是先执行类的 new 方法,再执行类的 init 即可。
因此,我们可以在 new 上做一些事情,使得类的实例只能存在一个。
如下是一个示例,在第一次实例的时候,会在 cls 上添加一个属性 _instance
来保存第一个实例,后面再实例化时,就会返回第一次创建的时候,
class Config(object):
def __new__(cls, *args, **kwargs):
if not hasattr(cls, '_instance'):
cls._instance = super().__new__(cls, *args, **kwargs)
return cls._instance
cfg1 = Config()
cfg2 = Config()
print(cfg1 == cfg2)
运行后,输出结果 True
细心的朋友,想必已经发现,如果有其他类继承了 Config 这个类,那么 _instance
也同样会被子类继承过去,这会导致只要 Config 及其子类只能有一个实例,只要 Config 实例过了,其子类就不能再实例化了,但这显然不是我们所期望的。
有的朋友,可能会想到用双下划线的 __instance
,这样就不会被继承了。
很遗憾的是,双下划线的属性,虽然不会被继承,但却有一个问题,就是属性名会被 Python 修改掉,变成 _Config__instance
,这样一样,我们在编写 new 时就无法使用 hasattr 来判断。
class Config(object):
def __new__(cls, *args, **kwargs):
if not hasattr(cls, '__instance'):
cls.__instance = super().__new__(cls, *args, **kwargs)
return cls.__instance
cfg1 = Config()
print(hasattr(Config, "__instance")) # 输出:False
print(hasattr(Config, "_Config__instance")) # 输出:True
最好的做法是:在所有的子类将 _instance
重置为 None,并且重写 __new__
方法,最重要的是不要去调用 supper.__new__()
class Config(object):
def __new__(cls, *args, **kwargs):
if not hasattr(cls, '_instance'):
cls._instance = super().__new__(cls, *args, **kwargs)
return cls._instance
class ConfigExt(Config):
_instance = None
def __new__(cls, *args, **kwargs):
if not hasattr(cls, '_instance'):
cls._instance = object.__new__(cls, *args, **kwargs)
return cls._instance
cfg1 = Config()
cfg2 = ConfigExt()
cfg3 = ConfigExt()
print(cfg1 == cfg2) # False
print(cfg2 == cfg3) # True
可以看到,通过修改 __new__
方法,已经可以解决装饰器不能被继承的问题,但在有可能被继承的类里,却存在安全隐患。
4. 通过 metaclass
metaclass 是什么呢? 用一句话说明
- 类,是用来创建实例对象的「模板」。
- 而元类,是创建类的「模板」。
因此我们可以通过修改元类,来使得创建此类时,就直接是一个单例类。
学习元类的作用过程,可以用普通类来做对比学习:
- 在创建一个实例时,会走类的 new 方法
- 同样地逻辑,在创建一个普通类时,也会走元类的 new
与此同时:
- 在类里实现了 call 可以让这个类的实例,变成可调用对象
- 对普通类进行实例化时,实际是对一个元类的实例(也就是普通类)进行直接调用,因此会走元类的 call 方法
如下是一个示例,可以发现,父类和子类都可以完美实现单例,而不需要有额外的约定,也不需要团队里的人有统一的技术积累,就像平时一样,不会有任何突兀的感觉。
class Singleton(type):
def __init__(cls, *args, **kwargs):
cls.__instance = None
super().__init__(*args, **kwargs)
def __call__(cls, *args, **kwargs):
if not cls.__instance:
cls.__instance = super().__call__(*args, **kwargs)
return cls.__instance
class Config(metaclass=Singleton):
pass
class ConfigExt(Config):
pass
cfg1 = Config()
cfg2 = Config()
cfg3 = ConfigExt()
cfg4 = ConfigExt()
print(cfg1 == cfg2) # True
print(cfg2 == cfg3) # False
print(cfg3 == cfg4) # True
另外,该方法在多线程场景下并发创建实例对象时,由于初始化时,是需要一点时间,那么就会导致数据不同步的问题,导致出现多个实例。
为了方便复现,我在 __call__
里加了 time.sleep(1) 来实现延长实例化的时间
import time
import threading
class Singleton(type):
def __init__(cls, *args, **kwargs):
cls.__instance = None
super().__init__(*args, **kwargs)
def __call__(cls, *args, **kwargs):
if not cls.__instance:
time.sleep(1)
cls.__instance = super().__call__(*args, **kwargs)
return cls.__instance
class Config(metaclass=Singleton):
pass
def task():
cfg = Config()
print(id(cfg))
for i in range(10):
t = threading.Thread(target=task)
t.start()
运行结果如下,说明单例模式出现了问题
4336068992
4373051424
4335324896
4336069088
4335324848
4336069088
4335324896
4336069088
4335324848
4336069088
但这个问题可以通过线程锁来解决,先定义一个装饰器 synchronized
,然后在元类里的 __call__
加上这个装饰器。
import time
import threading
def synchronized(func):
func.__lock__ = threading.Lock()
def lock_func(*args, **kwargs):
with func.__lock__:
return func(*args, **kwargs)
return lock_func
class Singleton(type):
def __init__(cls, *args, **kwargs):
cls.__instance = None
super().__init__(*args, **kwargs)
@synchronized
def __call__(cls, *args, **kwargs):
if not cls.__instance:
time.sleep(1)
cls.__instance = super().__call__(*args, **kwargs)
return cls.__instance
class Config(metaclass=Singleton):
pass
def task():
cfg = Config()
print(id(cfg))
for i in range(10):
t = threading.Thread(target=task)
t.start()
运行的结果就正常了
4381387136
4381387136
4381387136
4381387136
4381387136
4381387136
4381387136
4381387136
4381387136
4381387136
5. 总结一下
在 Python 中有很多种方法实现单例的效果,但不同的方法,却有不同的局限性:
- 使用模块:最简单直接且安全,推荐使用
- 使用装饰器:不能被继承,不推荐使用
- 使用 new 方法:需要开发成员对Python有足够的认识,不然代码会有BUG。
- 使用 metaclass:完美的单例实践,也推荐使用,但要注意加锁