共计 4389 个字符,预计需要花费 11 分钟才能阅读完成。
在前一篇 blog 中咱们提到了 logging 模块中的 root logger 是单例模式的,所以紧接着咱们就来摸索一下单例模式在 python 中的实现。
什么是单例模式(singleton)
单例模式是一种对于创建对象行为的设计模式,该模式要求一个类只能创立并存有一个实例化对象。
而为什么应用单例模式呢?
单例模式是为了防止不必要的内存节约,如多个线程调用一个类,但理论并不需要对每个线程都提供一个该类的实例;同样也是为了避免对资源的多重占用,如多个线程要操作同一个文件对象,那么创立多个句柄是不合理的。
单例模式的设计逻辑
那么如何设计一个单例模式呢?个别的有两个重要的设计逻辑:
- 饿汉模式:在类加载时或者至多在类被应用前创立一个单例实例。即不论前面有没有应用该类都创立一个实例。
- 懒汉模式:在第一次须要应用类的时候创立一个单例实例。
对于 java 与 c ++ 都有对应的饿汉模式与懒汉模式的实现,这里介绍一下它们的简略实现(不思考线程平安的状况)。
Java
//java 饿汉模式
public class Singleton {
// 间接在类中定义了 instance 作为 Singleton 实例,// 即在类加载时就创立出一个单例
private static Singleton instance = new Singleton();
// 暗藏构造函数
private Singleton(){}
// 裸露惟一能够失去类对象实例的接口 getInstance 动态函数
public static Singleton getInstance(){return instance;}
}
//java 懒汉模式
public class Singleton {
// 暗藏构造函数
private Singleton() {}
// 把 instance 初始化为 null
private static Singleton instance = null;
// 裸露惟一能够失去类对象实例的接口 getInstance 动态函数
public static Singleton getInstance() {
// 第一次被调用时,instance 是 null,所以 if 判断通过
if (instance == null) {
// 失去单例
instance = new Singleton();}
// 之后再调用该接口只会返回以后存在的单例
return instance;
}
}
C++
//c++ 饿汉模式
class Singleon
{
private:
// 暗藏构造函数接口
Singleon(){};
//C++ 中无奈在类定义中赋值,const static 的润饰办法也只能用于整型
// 定义一个动态类成员,不属于任何一个实例
static Singleon* instance;
public:
static Singleon* GetSingleon()
{return instance;}
static Singleon* Destroy()
{
delete instance;
instance = NULL;
}
};
// 留神这里,对于 c ++ 类中的静态数据成员,不管它拜访限度
// 是 private 还是 public 的,它的初始化都是以如下的模式
//(类型名)(类名)::(静态数据成员名)=(value);
// 而且这里的 new Singleton(),即便咱们曾经把构造函数
// 的拜访限度私有化了,在对静态数据成员初始化的时候
// 还是能够间接应用,之后在其它任何中央应用私有化的
// 构造函数接口都是非法的
Singleon* Singleon::instance = new Singleton() ;
//c++ 懒汉模式
class Singleon
{
private:
// 同样暗藏构造函数接口
Singleon(){};
static Singleon*instance;
public:
// 裸露的接口
static Singleon* GetSingleon()
{
// 判断静态数据成员 instance 的值
if (instance==NULL)
{instance = new Singleon();
}
return instance;
}
static Singleon* Destroy()
{
delete instance;
instance = NULL;
}
};
Singleon* Singleon::instance = NULL;
能够看出 java 间接在类内就能够初始化动态成员,所以在加载的时候就能够依照饿汉模式的设计间接生成一个类的单例,而 c ++ 不能在类内初始化静态数据成员为类实例,所以还须要在类定义外对其初始化,来造成单例设计模式。至于 python,和 c ++ 相似,它也不能在类内初始化属性为类的实例,如:
class Test:
t=Test()
.....
这段代码是谬误的,因为对于 python 在加载类的时候,把类内语句作为可执行的,而执行 t =Test()时,Test 类还未定义进去,因而矛盾出错。
python 类内 namespace 和 scope 的解析
在 python 中 namespace 和 scope 其实一体两面的概念,namespace 着眼于 name 与 object 之间的映射关系,确保 name 之间不存在 conflict,python 中存在三种 namespace:
- local namespace(函数内,类内)
- global namespace
- builtin namespace
而 scope 则形容的是变量(directly access)寻找的问题,它为此定义了一个 hierarchy:LEGB(Local->Enclosing->Global->Builtin)。
对于 python 中类内的 namespace 是失常的,属于 local namespace,然而对于类内的 scope,它是 LEGB 这个 hierarchy 中的一个非凡存在。上面咱们借由一段代码解释一下:
var=123
str="love you!"
class Test:
var=19
print(str)
def fun(self):
print(var)
t=Test()
t.fun()
--------------------------
下面这段代码执行后,输入如下:love you!
123
可见,类的 method 间接忽视了类内的 scope(当然用类名限定:Test.var 也是能够拜访到的,然而咱们当初探讨的 LEGB 是 directly access 的问题),间接找到的是 global 中的变量 var,而类内的 print(str)也能够找到 global 的 str。那么简略来说:1.python 类内 scope 对类内 method 不可见,也就是说类内定义的变量或是其余 method 都不能被某一个 method 间接援用。2.python 类外向外寻找是合乎 LEGB 规定的。
python 单例模式设计
咱们这里只简略介绍一下应用__new__办法进行单例设计的形式,当前若有工夫再进行补充。
首先 python 没法像 java 和 c ++ 一样把接口暗藏起来,同时也无奈在类定义中初始化类属性为类实例。以此为前提,咱们应用__new__办法来探讨单例模式的设计(new 其实相当于半个构造函数,只结构实例不初始化):
# 所谓饿汉模式
class Singleton:
def __new__(cls):
if not hasattr(Singleton,"instance"):
cls.instance=super().__new__(cls)
return cls.instance
#在应用前实例化
Singleton()
#懒汉模式 -> 能够说所谓的饿汉模式就是在懒汉模式后加了 Singleton()
class Singleton:
def __new__(cls):
if not hasattr(Singleton,"instance"):
cls.instance=super().__new__(cls)
return cls.instance
看完以上的代码兴许你会感觉有些牵强,感觉 python 的饿汉模式有些“假”,然而饿汉懒汉其实都是一种设计逻辑罢了,只有实现了相应的逻辑,具体的代码之间的区别并不重要!
线程平安的 python 单例模式
这里须要对 python 单例模式的线程平安进行进一步的介绍,因为下面咱们介绍的__new__办法设计单例模式的饿汉懒汉代码差异不大,咱们这里就以懒汉模式进行线程平安的介绍,看代码:
import time
import threading
class Singleton:
def __new__(cls):
#假如有两个 thread 都一次执行到了这个 if 判断,#thead1 通过,而后继续执行 time.sleep(1),#那么在这个 1 秒的 sleep 中,thread2 也通过了
#这个 if 判断,则前面很显然的会创立两个实例
if not hasattr(Singleton,"instance"):
time.sleep(1)
cls.instance=super().__new__(cls)
return cls.instance
针对以上状况,咱们应用线程锁进行改良
class Singleton:
lock=threading.lock
def __new__(cls):
#还是雷同的状况,两个 thead 执行到这个判断,#thead1 先通过,执行下一句 with threading.Lock()
#那么便间接占有了锁,之后在 thread1 的 1 秒 sleep 中
#thread2 也通过了第一个 if 判断,而继续执行执行
#with threading.Lock()语句,无奈抢占锁,被阻塞
#当 thread1 实现 1 秒的 sleep 后,并且通过第二个 if,#对 cls.instance 赋值,退出 with context 后,thread2
#能力继续执行,1 秒 sleep 之后再进行第二个 if 判断,#此时不能通过了,因为 thread1 曾经创立了一个 instance
#那么只好退出 with context,再执行 return cls.instance
#其实就是返回 thread1 创立的 cls.instance
if not hasattr(Singleton,"instance"):
#threading.Lock()反对 context manager protocol
with Singleton.Lock():
time.sleep(1)
if not hasattr(Singleton,"instance"):
cls.instance=super().__new__(cls)
return cls.instance
以上的单例模式线程平安实现办法叫做:双重检测机制 。两个 if 判断,其中 第一个 if 判断是为了避免每个 thread 都要进行抢占锁而后执行对应代码,会很浪费时间;而 第二个if 判断则更加要害,如果有多个 thread 都通过了第一个 if 判断,进入锁的抢占,如果没有第二个 if 判断,那么还是会每个 thread 生成一个实例,无奈实现单例模式。