相關(guān)代碼已經(jīng)上傳至Github:Python_Development_Interview,大家可以收藏專題-Python的設(shè)計模式:解密+實(shí)戰(zhàn)荣病,之后會持續(xù)更新相關(guān)的設(shè)計模式抗愁。
1. 認(rèn)識單例模式
2. Python實(shí)現(xiàn)單例模式
3. 總結(jié)
認(rèn)識單例模式
1.1 單例模式含義
單例模式,也叫單子模式鲤孵,是一種常用的軟件設(shè)計模式。在應(yīng)用這個模式時辰如,單例對象的類必須保證只有一個實(shí)例存在普监。許多時候整個系統(tǒng)只需要擁有一個的全局對象,這樣有利于我們協(xié)調(diào)系統(tǒng)整體的行為琉兜。比如在某個服務(wù)器程序中鹰椒,該服務(wù)器的配置信息存放在一個文件中,這些配置數(shù)據(jù)由一個單例對象統(tǒng)一讀取呕童,然后服務(wù)進(jìn)程中的其他對象再通過這個單例對象獲取這些配置信息漆际。這種方式簡化了在復(fù)雜環(huán)境下的配置管理。
實(shí)現(xiàn)單例模式的思路是:一個類能返回對象一個引用(永遠(yuǎn)是同一個)和一個獲得該實(shí)例的方法(必須是靜態(tài)方法夺饲,通常使用getInstance這個名稱)奸汇;當(dāng)我們調(diào)用這個方法時,如果類持有的引用不為空就返回這個引用往声,如果類保持的引用為空就創(chuàng)建該類的實(shí)例并將實(shí)例的引用賦予該類保持的引用擂找;同時我們還將該類的構(gòu)造函數(shù)定義為私有方法,這樣其他處的代碼就無法通過調(diào)用該類的構(gòu)造函數(shù)來實(shí)例化該類的對象浩销,只有通過該類提供的靜態(tài)方法來得到該類的唯一實(shí)例贯涎。
單例模式在多線程的應(yīng)用場合下必須小心使用。如果當(dāng)唯一實(shí)例尚未創(chuàng)建時慢洋,有兩個線程同時調(diào)用創(chuàng)建方法塘雳,那么它們同時沒有檢測到唯一實(shí)例的存在陆盘,從而同時各自創(chuàng)建了一個實(shí)例,這樣就有兩個實(shí)例被構(gòu)造出來败明,從而違反了單例模式中實(shí)例唯一的原則隘马。 解決這個問題的辦法是為指示類是否已經(jīng)實(shí)例化的變量提供一個互斥鎖(雖然這樣會降低效率)。
1.2 單例模式優(yōu)點(diǎn)
單例模式的優(yōu)點(diǎn):
1妻顶、由于單例模式要求在全局內(nèi)只有一個實(shí)例酸员,因而可以節(jié)省比較多的內(nèi)存空間;
2讳嘱、全局只有一個接入點(diǎn)幔嗦,可以更好地進(jìn)行數(shù)據(jù)同步控制,避免多重占用沥潭;
3邀泉、單例可長駐內(nèi)存,減少系統(tǒng)開銷叛氨。
1.3 單例模式缺點(diǎn)
單例模式的缺點(diǎn)
1呼渣、單例模式的擴(kuò)展是比較困難的棘伴;
2寞埠、賦于了單例以太多的職責(zé),某種程度上違反單一職責(zé)原則(六大原則后面會講到);
3焊夸、單例模式是并發(fā)協(xié)作軟件模塊中需要最先完成的仁连,因而其不利于測試;
4阱穗、單例模式在某種情況下會導(dǎo)致“資源瓶頸”饭冬。
1.4 單例模式應(yīng)用
單例模式的應(yīng)用舉例:
1、生成全局惟一的序列號揪阶;
2昌抠、訪問全局復(fù)用的惟一資源,如磁盤鲁僚、總線等炊苫;
3、單個對象占用的資源過多冰沙,如數(shù)據(jù)庫等侨艾;
4、系統(tǒng)全局統(tǒng)一管理拓挥,如Windows下的Task Manager唠梨;
5、網(wǎng)站計數(shù)器侥啤。
Python實(shí)現(xiàn)單例模式
2.1 多種實(shí)現(xiàn)方法
2.1.1.使用模塊
其實(shí)当叭,Python 的模塊就是天然的單例模式茬故,因?yàn)槟K在第一次導(dǎo)入時,會生成 .pyc 文件科展,當(dāng)?shù)诙螌?dǎo)入時均牢,就會直接加載 .pyc 文件,而不會再次執(zhí)行模塊代碼才睹。因此徘跪,我們只需把相關(guān)的函數(shù)和數(shù)據(jù)定義在一個模塊中,就可以獲得一個單例對象了琅攘。如果我們真的想要一個單例類垮庐,可以考慮這樣做:
singleton_by_module.py
class Singleton(object):
def foo(self):
pass
singleton = Singleton()
將上面的代碼保存在文件 singleton_by_module.py
中,要使用時坞琴,直接在其他文件中導(dǎo)入此文件中的對象哨查,這個對象即是單例模式的對象
test_singleton_by_module.py
from singleton_by_module import Singleton
t = Singleton()
這樣我們一旦調(diào)用到singleton_by_module.py
就會產(chǎn)生一個singleton_by_module.pyc
,以后我們每次調(diào)用都會直接引用這里面的代碼剧辐。
2.1.2.使用裝飾器
singleton_by_decorator.py
def Singleton(cls):
_instance = {}
count = 0
def _singleton(*args, **kargs):
nonlocal count
if cls not in _instance:
print(f"count: {count}: {cls.__name__} not init")
_instance[cls] = cls(*args, **kargs)
else:
print(f"count: {count}: {cls.__name__} alreay init")
count+=1
return _instance[cls]
return _singleton
@Singleton
class A(object):
a = 1
def __init__(self, x=0):
self.x = x
a1 = A(2)
a2 = A(3)
print(f"a1 id: {id(a1)}, a1 value: {a1.x}")
print(f"a2 id: {id(a2)}, a2 value: {a2.x}")
### output
count: 0: A not init
count: 1: A alreay init
a1 id: 140536039677232, a1 value: 2
a2 id: 140536039677232, a2 value: 2
根據(jù)上面的運(yùn)行情況寒亥,我們可以發(fā)現(xiàn),當(dāng)a1被創(chuàng)建后調(diào)用的是正常的產(chǎn)生實(shí)例的過程荧关,當(dāng)a2被創(chuàng)建的時候溉奕,由于之前實(shí)例已經(jīng)被存儲下來,所以直接引用了a1的實(shí)例忍啤,所以他們的id是一樣的加勤,也就是他們引用了同一個內(nèi)存實(shí)例。
2.1.3.使用類
singleton_by_class.py
class Singleton:
def __init__(self):
pass
@classmethod
def instance(cls, *args, **kwargs):
if not hasattr(Singleton, "_instance"):
Singleton._instance = Singleton(*args, **kwargs)
return Singleton._instance
a1 = Singleton.instance()
a2 = Singleton.instance()
print(f"a1 id: {id(a1)}")
print(f"a2 id: {id(a2)}")
### output
a1 id: 140419818871776
a2 id: 140419818871776
一般情況同波,大家以為這樣就完成了單例模式鳄梅,但是這樣當(dāng)使用多線程時會存在問題
singleton_by_class_mutli_threading.py
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
import threading
def task(arg):
obj = Singleton.instance()
print(obj)
for i in range(10):
t = threading.Thread(target=task,args=[i,])
t.start()
程序執(zhí)行后,打印結(jié)果如下:
<__main__.Singleton object at 0x02C933D0>
<__main__.Singleton object at 0x02C933D0>
<__main__.Singleton object at 0x02C933D0>
<__main__.Singleton object at 0x02C933D0>
<__main__.Singleton object at 0x02C933D0>
<__main__.Singleton object at 0x02C933D0>
<__main__.Singleton object at 0x02C933D0>
<__main__.Singleton object at 0x02C933D0>
<__main__.Singleton object at 0x02C933D0>
<__main__.Singleton object at 0x02C933D0>
看起來也沒有問題未檩,那是因?yàn)閳?zhí)行速度過快戴尸,如果在init方法中有一些IO操作,就會發(fā)現(xiàn)問題了冤狡,下面我們通過time.sleep模擬
我們在上面init方法中加入以下代碼:
singleton_by_class_mutli_threading_sleep.py
def __init__(self):
import time
time.sleep(1)
重新執(zhí)行程序后孙蒙,結(jié)果如下
<__main__.Singleton object at 0x034A3410>
<__main__.Singleton object at 0x034BB990>
<__main__.Singleton object at 0x034BB910>
<__main__.Singleton object at 0x034ADED0>
<__main__.Singleton object at 0x034E6BD0>
<__main__.Singleton object at 0x034E6C10>
<__main__.Singleton object at 0x034E6B90>
<__main__.Singleton object at 0x034BBA30>
<__main__.Singleton object at 0x034F6B90>
<__main__.Singleton object at 0x034E6A90>
問題出現(xiàn)了!按照以上方式創(chuàng)建的單例筒溃,無法支持多線程
解決辦法:加鎖马篮!未加鎖部分并發(fā)執(zhí)行,加鎖部分串行執(zhí)行,速度降低,但是保證了數(shù)據(jù)安全
singleton_by_class_mutli_threading_lock.py
import time
import threading
class Singleton:
_instance_lock = threading.Lock()
def __init__(self):
time.sleep(1)
@classmethod
def instance(cls, *args, **kwargs):
with Singleton._instance_lock:
if not hasattr(Singleton, "_instance"):
Singleton._instance = Singleton(*args, **kwargs)
return Singleton._instance
def task(arg):
obj = Singleton.instance()
print(obj)
for i in range(10):
t = threading.Thread(target=task,args=[i,])
t.start()
time.sleep(20)
obj = Singleton.instance()
print(obj)
打印結(jié)果如下:
<__main__.Singleton object at 0x02D6B110>
<__main__.Singleton object at 0x02D6B110>
<__main__.Singleton object at 0x02D6B110>
<__main__.Singleton object at 0x02D6B110>
<__main__.Singleton object at 0x02D6B110>
<__main__.Singleton object at 0x02D6B110>
<__main__.Singleton object at 0x02D6B110>
<__main__.Singleton object at 0x02D6B110>
<__main__.Singleton object at 0x02D6B110>
<__main__.Singleton object at 0x02D6B110>
這樣就差不多了,但是還是有一點(diǎn)小問題怜奖,就是當(dāng)程序執(zhí)行時浑测,執(zhí)行了time.sleep(20)后,下面實(shí)例化對象時,此時已經(jīng)是單例模式了迁央,但我們還是加了鎖掷匠,這樣不太好,再進(jìn)行一些優(yōu)化岖圈,把intance方法讹语,改成下面的這樣就行:
@classmethod
def instance(cls, *args, **kwargs):
if not hasattr(Singleton, "_instance"):
with Singleton._instance_lock:
if not hasattr(Singleton, "_instance"):
Singleton._instance = Singleton(*args, **kwargs)
return Singleton._instance
這樣,一個可以支持多線程的單例模式就完成了
singleton_by_class_mutli_threading_safe.py
import time
import threading
class Singleton:
_instance_lock = threading.Lock()
def __init__(self):
time.sleep(1)
@classmethod
def instance(cls, *args, **kwargs):
if not hasattr(Singleton, "_instance"):
with Singleton._instance_lock:
if not hasattr(Singleton, "_instance"):
Singleton._instance = Singleton(*args, **kwargs)
return Singleton._instance
def task(arg):
obj = Singleton.instance()
print(obj)
for i in range(10):
t = threading.Thread(target=task,args=[i,])
t.start()
time.sleep(20)
obj = Singleton.instance()
print(obj)
完整代碼
這種方式實(shí)現(xiàn)的單例模式蜂科,使用時會有限制顽决,以后實(shí)例化必須通過 obj = Singleton.instance()
如果用 obj=Singleton() ,這種方式得到的不是單例
2.1.4基于new方法實(shí)現(xiàn)(推薦使用,方便)
通過上面例子导匣,我們可以知道才菠,當(dāng)我們實(shí)現(xiàn)單例時,為了保證線程安全需要在內(nèi)部加入鎖
我們知道贡定,當(dāng)我們實(shí)例化一個對象時赋访,是先執(zhí)行了類的new方法(我們沒寫時,默認(rèn)調(diào)用type.new)缓待,實(shí)例化對象蚓耽;然后再執(zhí)行類的init方法,對這個對象進(jìn)行初始化旋炒,所有我們可以基于這個步悠,實(shí)現(xiàn)單例模式
singleton_by_new.py
import threading
class Singleton:
_instance_lock = threading.Lock()
def __init__(self):
pass
def __new__(cls, *args, **kwargs):
if not hasattr(Singleton, "_instance"):
with Singleton._instance_lock:
if not hasattr(Singleton, "_instance"):
Singleton._instance = super(Singleton,cls).__new__(cls,*args, **kwargs)
return Singleton._instance
obj1 = Singleton()
obj2 = Singleton()
print(obj1,obj2)
def task(arg):
obj = Singleton()
print(obj)
for i in range(10):
t = threading.Thread(target=task,args=[i,])
t.start()
打印結(jié)果如下:
<__main__.Singleton object at 0x038B33D0> <__main__.Singleton object at 0x038B33D0>
<__main__.Singleton object at 0x038B33D0>
<__main__.Singleton object at 0x038B33D0>
<__main__.Singleton object at 0x038B33D0>
<__main__.Singleton object at 0x038B33D0>
<__main__.Singleton object at 0x038B33D0>
<__main__.Singleton object at 0x038B33D0>
<__main__.Singleton object at 0x038B33D0>
<__main__.Singleton object at 0x038B33D0>
<__main__.Singleton object at 0x038B33D0>
<__main__.Singleton object at 0x038B33D0>
采用這種方式的單例模式,以后實(shí)例化對象時国葬,和平時實(shí)例化對象的方法一樣 obj = Singleton()
2.1.5.基于metaclass方式實(shí)現(xiàn)
相關(guān)知識
"""
1.類由type創(chuàng)建贤徒,創(chuàng)建類時芹壕,type的init方法自動執(zhí)行汇四,類() 執(zhí)行type的 call方法(類的new方法,類的init方法)
2.對象由類創(chuàng)建,創(chuàng)建對象時踢涌,類的init方法自動執(zhí)行通孽,對象()執(zhí)行類的 call 方法
"""
class Foo:
def __init__(self):
pass
def __call__(self, *args, **kwargs):
pass
obj = Foo()
# 執(zhí)行type的 __call__ 方法,調(diào)用 Foo類(是type的對象)的 __new__方法睁壁,用于創(chuàng)建對象背苦,然后調(diào)用 Foo類(是type的對象)的 __init__方法,用于對對象初始化潘明。
obj() # 執(zhí)行Foo的 __call__ 方法
元類的使用
metaclass_ex.py
class SingletonType(type):
def __init__(self,*args,**kwargs):
super(SingletonType,self).__init__(*args,**kwargs)
def __call__(cls, *args, **kwargs): # 這里的cls行剂,即Foo類
print('cls',cls)
obj = cls.__new__(cls,*args, **kwargs)
cls.__init__(obj,*args, **kwargs) # Foo.__init__(obj)
return obj
class Foo(metaclass=SingletonType): # 指定創(chuàng)建Foo的type為SingletonType
def __init__(self,name):
self.name = name
def __new__(cls, *args, **kwargs):
return object.__new__(cls)
obj = Foo('xx')
實(shí)現(xiàn)單例模式
singleton_by_metaclass.py
import threading
class SingletonType(type):
_instance_lock = threading.Lock()
def __call__(cls, *args, **kwargs):
if not hasattr(cls, "_instance"):
with SingletonType._instance_lock:
if not hasattr(cls, "_instance"):
cls._instance = super(SingletonType,cls).__call__(*args, **kwargs)
return cls._instance
class Foo(metaclass=SingletonType):
def __init__(self,name):
self.name = name
obj1 = Foo('name')
obj2 = Foo('name')
print(obj1,obj2)
2.2 實(shí)例分析
總線是計算機(jī)各種功能部件或者設(shè)備之間傳送數(shù)據(jù)、控制信號等信息的公共通信解決方案之一∏担現(xiàn)假設(shè)有如下場景:某中央處理器(CPU)通過某種協(xié)議總線與一個信號燈相連厚宰,信號燈有64種顏色可以設(shè)置,中央處理器上運(yùn)行著三個線程,都可以對這個信號燈進(jìn)行控制铲觉,并且可以獨(dú)立設(shè)置該信號燈的顏色澈蝙。抽象掉協(xié)議細(xì)節(jié)(用打印表示),如何實(shí)現(xiàn)線程對信號等的控制邏輯撵幽。
加線程鎖進(jìn)行控制灯荧,無疑是最先想到的方法,但各個線程對鎖的控制盐杂,無疑加大了模塊之間的耦合逗载。下面,我們就用設(shè)計模式中的單例模式链烈,來解決這個問題撕贞。
代碼如下:
import threading
import time
#這里使用方法__new__來實(shí)現(xiàn)單例模式
class Singleton(object):#抽象單例
def __new__(cls, *args, **kw):
if not hasattr(cls, '_instance'):
orig = super(Singleton, cls)
cls._instance = orig.__new__(cls, *args, **kw)
return cls._instance
#總線
class Bus(Singleton):
lock = threading.RLock()
def sendData(self,data):
self.lock.acquire()
time.sleep(3)
print "Sending Signal Data...",data
self.lock.release()
#線程對象,為更加說明單例的含義测垛,這里將Bus對象實(shí)例化寫在了run里
class VisitEntity(threading.Thread):
my_bus=""
name=""
def getName(self):
return self.name
def setName(self, name):
self.name=name
def run(self):
self.my_bus=Bus()
self.my_bus.sendData(self.name)
if __name__=="__main__":
for i in range(3):
print "Entity %d begin to run..."%i
my_entity=VisitEntity()
my_entity.setName("Entity_"+str(i))
my_entity.start()
運(yùn)行結(jié)果如下:
Entity 0 begin to run...
Entity 1 begin to run...
Entity 2 begin to run...
Sending Signal Data... Entity_0
Sending Signal Data... Entity_1
Sending Signal Data... Entity_2
在程序運(yùn)行過程中捏膨,三個線程同時運(yùn)行(運(yùn)行結(jié)果的前三行先很快打印出來),而后分別占用總線資源(后三行每隔3秒打印一行)食侮。雖然看上去總線Bus被實(shí)例化了三次号涯,但實(shí)際上在內(nèi)存里只有一個實(shí)例。
總結(jié)
因?yàn)閱卫J皆谠O(shè)計模式中算是最基礎(chǔ)且最簡單的一個模式锯七,因此在一般初級面試的時候链快,面試官都會通過這個問題來考察,一個很重要的原因是單例模式實(shí)現(xiàn)方法多種且優(yōu)化的方式也有很多眉尸,所以也很能考察應(yīng)聘者的水平域蜗,所以,大家要好好學(xué)這個最基礎(chǔ)的設(shè)計模式霸牖霉祸!另外,在Java
中單例模式常說的飽漢*餓漢*模式袱蜡,其實(shí)和Python
中的利用__new__
和利用class
來創(chuàng)建是一樣的丝蹭,也就是在什么時候創(chuàng)建實(shí)例的區(qū)別。