Python單例模式的設(shè)計(jì)與實(shí)現(xiàn)【完美版】

@[toc]

1. 按

眾所周知,對(duì)象是解決繼承的問題的钳枕,如果沒有對(duì)象,那么將很難解決繼承的問題赏壹。這點(diǎn)兒和人類的現(xiàn)實(shí)世界有相似之處鱼炒,沒有對(duì)象的話何談子嗣的繼承問題?
沒有對(duì)象蝌借,將意味著很難實(shí)現(xiàn)繼承時(shí)的多態(tài)昔瞧、很難去塑造子類,沒有子類又將意味著父類在設(shè)計(jì)時(shí)要具備所有的功能骨望,這絕對(duì)是極不現(xiàn)實(shí)的硬爆。一種很現(xiàn)實(shí)的設(shè)計(jì)是:父類先解決一些問題,然后子類再解決一些問題擎鸠,接著子類的子類再解決一些問題缀磕,這樣子子孫孫無窮盡也,一定能夠把所有問題都解決好的劣光,這種設(shè)計(jì)模式也一定能夠應(yīng)對(duì)復(fù)雜多變的現(xiàn)實(shí)環(huán)境袜蚕,因此對(duì)象的存在意義重大。
但對(duì)象是越多越好嗎绢涡?答案絕對(duì)是否定的牲剃,很多情況下我們只需要一個(gè)對(duì)象就可以了,多余的對(duì)象會(huì)帶來很多的不必要的開銷和麻煩雄可。這是因?yàn)槊總€(gè)對(duì)象都要占據(jù)一定的內(nèi)存空間和CPU算力凿傅,大多數(shù)情況下我們只需要一個(gè)對(duì)象去執(zhí)行任務(wù),對(duì)于一顆CPU的核心而言数苫,操作一個(gè)對(duì)象是最快的聪舒,為什么?這主要是由于線程的切換會(huì)造成不必要的路程開銷虐急。
設(shè)想一下箱残,假如你有一個(gè)對(duì)象可以用來接吻,你的DNA序列約定你一生要完成4800次接吻才算完成了任務(wù)止吁。這樣在你臨死的時(shí)候才會(huì)死而無憾被辑,即程序完成所有的任務(wù)后正常退出,返回值為0敬惦,代表剩余待做的任務(wù)數(shù)為0盼理。平均你和一個(gè)對(duì)象每天的最多能接吻48次,這樣只和單個(gè)對(duì)象全力接吻的話100天就能完成任務(wù)俄删。
但如果你有多個(gè)對(duì)象的話榜揖,你的平均作戰(zhàn)能力并不會(huì)提升勾哩,這是因?yàn)槊刻旖游?8次是你的處理極限,相反举哟,你因?yàn)橐投鄠€(gè)對(duì)象進(jìn)行接吻,每次從一個(gè)對(duì)象移動(dòng)到另一個(gè)對(duì)象那里會(huì)產(chǎn)生不必要的路程開銷迅矛,會(huì)大大影響你的工作效率妨猩。
假設(shè)你有兩個(gè)對(duì)象,對(duì)象A在河南秽褒,對(duì)象B在河北壶硅,你從河南到河北需要一天的時(shí)間,你每天工作完了之后會(huì)切換到另一個(gè)對(duì)象那里销斟。假設(shè)先從與對(duì)象A接吻開始庐椒,工作一天共接吻48次,但第二天你需要移動(dòng)到對(duì)象B那里蚂踊,我們知道约谈,移動(dòng)的過程中是沒有對(duì)象可以接吻的,只有到達(dá)目的地找到人之后才能進(jìn)入工作狀態(tài)犁钟。
你切換任務(wù)的過程將消耗一天的時(shí)間棱诱,這一天等于白白浪費(fèi)了。因?yàn)槟憬裉觳]有執(zhí)行有意義的接吻工作涝动,雖然你也在馬不停蹄地忙碌迈勋,但那用于移動(dòng)距離、尋找目標(biāo)的忙碌對(duì)完成接吻任務(wù)而言是不必要的醋粟,也是沒有意義的靡菇。這樣的話,你是一天干活米愿,一天用于切換任務(wù)厦凤,即從當(dāng)前對(duì)象跑到另一個(gè)對(duì)象那里,平均下來吗货,兩天只能完成48次的接吻任務(wù)泳唠,這樣的話你需要花費(fèi)200天才能完成DNA上面約定的接吻任務(wù)量,效率比著單對(duì)象模式大打折扣宙搬。
這里只考慮完工作一天之后才切換目標(biāo)的情況笨腥,假如你沒有太多耐心,打一槍立馬就換地方勇垛,即完成一次接吻之后立即就跑到另一個(gè)對(duì)象那里脖母。這樣的話你將消耗4900天的時(shí)間才能完成任務(wù),效率極低闲孤,需要13年多才能完成任務(wù)谆级。
再假設(shè)你有多個(gè)對(duì)象烤礁,你的耐心很少,打一槍換十個(gè)地方肥照,即完成一次接吻之后脚仔,立馬跑到下一個(gè)對(duì)象那里,但發(fā)現(xiàn)這個(gè)對(duì)象沒化妝舆绎、沒打扮鲤脏,很難看,然后再立馬跑到下一個(gè)對(duì)象那里吕朵,平均見十個(gè)對(duì)象才碰到一個(gè)滿意的猎醇,這樣的話你將消耗48100天才能完成任務(wù)全跨,效率更低了熔任,需要131多年才能完成任務(wù),怕是你這輩子不能死而無憾了拭抬,即返回值不能為0了梧税。
在假設(shè)你根本沒有耐心沦疾,打一槍換n個(gè)地方,即完成了一次接吻之后贡蓖,立馬跑到下一個(gè)對(duì)象那里曹鸠,但發(fā)現(xiàn)這個(gè)對(duì)象沒化妝、沒打扮斥铺,很難看彻桃,然后再立馬跑到下一個(gè)對(duì)象那里,接著就不斷地在重復(fù)這種死循環(huán)的奔波狀態(tài)晾蜘,因?yàn)楹翢o耐心邻眷,眼光又很高、很挑剔剔交,這輩子都在尋找合適的對(duì)象用于接吻肆饶,但一直都找不到。這種現(xiàn)象在計(jì)算機(jī)的狀態(tài)切換中被稱為死鎖岖常,死鎖是存在的驯镊,死鎖一旦出現(xiàn),程序就死了竭鞍,不會(huì)再執(zhí)行任務(wù)了板惑,陷入了一直切換狀態(tài)的情形中。這樣的話程序不論跑多久都不能完成任務(wù)偎快,強(qiáng)制退出或者意外中斷冯乘,返回值都不可能為0。

在計(jì)算機(jī)中晒夹,類的對(duì)象又稱為類的實(shí)例裆馒,因此我們把一個(gè)類只生成一個(gè)對(duì)象的模式稱為單例模式姊氓。

以下是常見的幾種創(chuàng)建單例的模式。
說明:我寫的懶漢式與餓漢式和別人的命名剛好是相反的喷好,這個(gè)感覺每個(gè)人的理解不同翔横,叫什么名字無所謂啦,只要能理解思想就行绒窑。

2. 本文地址

  1. 博客園:https://www.cnblogs.com/coco56/p/11253656.html
  2. 簡(jiǎn)書:http://www.reibang.com/p/4c47f8e3809b
  3. CSDN:https://blog.csdn.net/COCO56/article/details/97409050

3. 通過繼承單例父類來實(shí)現(xiàn)

實(shí)測(cè)發(fā)現(xiàn)通過繼承的方法來實(shí)現(xiàn)Python程序的單例模式設(shè)計(jì)是最完美的棕孙,之前嘗試過使用裝飾器來實(shí)現(xiàn)單例模式,但具體實(shí)踐的時(shí)候會(huì)出現(xiàn)諸多問題些膨。
比如如果在裝飾器中添加了getInstance方法和Instance屬性,那主流的IDE或者編輯器將無法推導(dǎo)出來有這個(gè)方法或?qū)傩郧掌蹋M管你的語(yǔ)法是正確的订雾,但由于編輯器無法推導(dǎo)出來,你將在編寫代碼的時(shí)候無法使用tab鍵快速補(bǔ)全矛洞,很不方便洼哎。
自身是為了自身的存在而存在的,用自身的存在去完善和改良自身的存在無疑是最好的選擇沼本。
前面介紹過噩峦,對(duì)象的存在其實(shí)是為了更好地解決繼承的問題的。既然是解決繼承的問題抽兆,那么無疑最好的選擇就是用繼承本身去解決繼承识补。
具體做法為:建一個(gè)單例父類,這個(gè)類只解決單例模式的問題辫红,然后所有需要使用單例模式的類全部繼承自這個(gè)單例父類凭涂。
相當(dāng)于老祖先把單例模式的問題解決了,孩子們只需要繼承老祖先的基因就可以了贴妻,這樣切油,孩子們天生就是單例模式(因?yàn)槔献嫦瘸私鉀Q單例問題,其他什么問題都不去做名惩,相當(dāng)于窮盡畢生的力量將單例問題研究地透透的澎胡,孩子們因?yàn)槔^承自老祖先,本身肯定已經(jīng)保留了老祖先的優(yōu)良基因)娩鹉。
經(jīng)實(shí)際測(cè)試攻谁,這種用繼承本身去解決繼承相關(guān)問題的方法是完美的。
示例代碼:

# -*- coding : utf-8 -*-
import threading

_instances = {}#相當(dāng)于民政局的登記簿底循,用于記錄每個(gè)單例類的專屬對(duì)象
_lock = threading.Lock()#使用線程鎖以確保線程安全

class SingletonClass(object):
    global _instances, _lock

    @classmethod
    def __new__(cls, *args, **kwargs):
        print('__new__ of', cls, *args, **kwargs)
        if cls in _instances: return _instances[cls]
        _lock.acquire()#上鎖
        _instances[cls] = cls._instances = super().__new__(cls)
        _lock.release()#解鎖
        return _instances[cls]


    @classmethod #用于刪除對(duì)象巢株,此方法僅在必要時(shí)使用,因?yàn)槿绻磸?fù)的析構(gòu)和構(gòu)造對(duì)象的話是極其浪費(fèi)資源的熙涤。
    def delInstance(cls, *args, **kwargs):
        if cls in _instances: _instances[cls].__del__(*args, **kwargs); del _instances[cls]

    @classmethod #用于獲取對(duì)象
    def getInstance(cls, *args, **kwargs):
        if cls in _instances: return _instances[cls]
        return cls(*args, **kwargs)

class CopyTree(SingletonClass):
    def __init__(self, *args, **kwargs): print('__init__ of', self, *args, **kwargs)

    def __del__(self, *args, **kwargs): print('__del__ of', self, *args, **kwargs)

class CopySubTree(CopyTree):
    pass

class CopyBCSubTree(CopyTree):
    pass

if __name__ == "__main__":
    #調(diào)用構(gòu)造函數(shù)和調(diào)用getInstance方法雖然都是獲得的單例阁苞。但對(duì)于已存在的對(duì)象來說:調(diào)用構(gòu)造函數(shù)困檩,會(huì)重新調(diào)用__init__方法再初始化一遍兒此對(duì)象。

    cls =CopyTree; print(cls); a = cls(); b = cls(); print('a=', a, 'b=', b)
    c = cls.getInstance(); d = cls.getInstance();  print('c=', c, 'd=', d, '\n')

    cls =CopySubTree; print(cls); a = cls(); b = cls(); print('a=', a, 'b=', b)
    c = cls.getInstance(); d = cls.getInstance();  print('c=', c, 'd=', d, '\n')

    cls =CopyBCSubTree; print(cls); a = cls(); b = cls(); print('a=', a, 'b=', b)
    c = cls.getInstance(); d = cls.getInstance();  print('c=', c, 'd=', d, '\n')

    print('\nbefore del:\n', _instances); cls.delInstance(); print('after del:\n', _instances);

4. 使用裝飾器實(shí)現(xiàn)

4.1. 懶漢式

懶漢式就是說“國(guó)家”分配對(duì)象那槽,在你還未出生的時(shí)候就已經(jīng)被指腹為婚了悼沿,這樣在你出生的時(shí)候就立即擁有了一個(gè)對(duì)象了,再也不用發(fā)愁對(duì)象的事兒了骚灸,這種不需要自己主動(dòng)付出就能得到對(duì)象的模式被稱為懶漢模式糟趾。
為了方便,這里我是用裝飾器甚牲,對(duì)需要的類進(jìn)行裝飾义郑,達(dá)到了一次定義,以后再處處使用時(shí)只需要一行代碼的效果丈钙。
優(yōu)點(diǎn):省心非驮,開始的時(shí)候一人分一個(gè)對(duì)象就好了,很省事雏赦。
缺點(diǎn):構(gòu)造函數(shù)只能是無參的劫笙,自己有想法,想傳個(gè)參數(shù)的話是不好傳過去的星岗。這是因?yàn)樵谠O(shè)計(jì)時(shí)就沒考慮你有想法的情況下填大,別管三七二十一,上來就給你分一個(gè)對(duì)象俏橘。

def Singleton(cls):
    print("正在進(jìn)行裝飾類", cls, '哦~')
    cls._instance = cls()#需要傳參數(shù)的話在這里改一下參數(shù)列表允华,但那樣的話就不具備如此廣泛的通用性了,我們想要的效果是一次定義裝飾器敷矫,以后對(duì)所有的類都適用例获。
    print('類', cls, '裝飾完畢,并且我們還給它分配了一個(gè)對(duì)象', cls._instance, '\n')

    def _singleton(): return cls._instance
    return _singleton

@Singleton
class A(object):
    @classmethod
    def __new__(cls, *args, **kwargs):
        print('__new__ of ', cls)
        return super().__new__(cls)

    def __init__(self, *args, **kwargs):
        print('__init__ of', self)
    

@Singleton
class B(object):
    @classmethod
    def __new__(cls, *args, **kwargs):
        print('__new__ of ', cls)
        return super().__new__(cls)

    def __init__(self, *args, **kwargs):
        print('__init__ of', self)


if __name__ == '__main__':
    print()

    a1 = A(); a2 = A(); print(a1, a2)
    print()

    b1 = B(); b2 = B(); print(b1, b2)

4.2. 餓漢式

餓漢式是指你餓了才給你分一個(gè)對(duì)象曹仗,把對(duì)象只分配給那些饑渴難耐大漢們(不分的話可能會(huì)出現(xiàn)問題榨汤,畢竟大漢如果發(fā)起情來還是很騷的,恐怕難以招架)怎茫。
優(yōu)點(diǎn):節(jié)省空間收壕,物盡其用,需要時(shí)才給你轨蛤,如果不需要想單身一輩子的話就不給你分配對(duì)象了蜜宪。
缺點(diǎn):可能存在線程不安全,饑渴難耐的大漢如果在分配對(duì)象的時(shí)候一下子多占了多個(gè)不同的對(duì)象怎么辦祥山?

4.2.1. 未加鎖版

一般在初始化對(duì)象的時(shí)候如果不進(jìn)行IO操作圃验,是沒事兒的。(即init方法里沒有IO操作)
這個(gè)版本實(shí)現(xiàn)簡(jiǎn)單缝呕,執(zhí)行速度也快澳窑,不用來回上鎖了斧散。加鎖版的就是給大漢分對(duì)象的時(shí)候先把大漢五花大綁地鎖起來,這樣的話就避免了在分配時(shí)他搶占多個(gè)對(duì)象的可能摊聋。分配完了之后再給大漢松下綁鸡捐、解下鎖,這樣步驟一多麻裁,肯定是比較耗時(shí)的箍镜。
示例1:裝飾時(shí)給每個(gè)類創(chuàng)建一個(gè)_instance屬性

def Singleton(cls):
    print("正在進(jìn)行裝飾類", cls, '哦~')
    
    cls._instance = None

    def _singleton(*args, **kargs):
        if cls._instance: return cls._instance
        cls._instance = cls(*args, **kargs)
        return cls._instance

    return _singleton

@Singleton
class A(object):
    @classmethod
    def __new__(cls, *args, **kwargs):
        print('__new__ of ', cls)
        return super().__new__(cls)

    def __init__(self, *args, **kwargs):
        print('__init__ of', self)
    

@Singleton
class B(object):
    @classmethod
    def __new__(cls, *args, **kwargs):
        print('__new__ of ', cls)
        return super().__new__(cls)

    def __init__(self, *args, **kwargs):
        print('__init__ of', self)


if __name__ == '__main__':
    print()

    a1 = A(); a2 = A(); print(a1, a2)
    print()

    b1 = B(); b2 = B(); print(b1, b2)

示例2:裝飾給創(chuàng)建一個(gè)空字典,生成對(duì)象時(shí)再把這個(gè)對(duì)象添加到字典里煎源,下次如果查到字典里有所需對(duì)象的話就直接返回了色迂,沒有的話創(chuàng)建后再返回。

_instance = {}

def Singleton(cls):
    print("正在進(jìn)行裝飾類", cls, '哦~')

    global _instance

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


@Singleton
class A(object):
    @classmethod
    def __new__(cls, *args, **kwargs):
        print('__new__ of ', cls)
        return super().__new__(cls)

    def __init__(self, *args, **kwargs):
        print('__init__ of', self)
    

@Singleton
class B(object):
    @classmethod
    def __new__(cls, *args, **kwargs):
        print('__new__ of ', cls)
        return super().__new__(cls)

    def __init__(self, *args, **kwargs):
        print('__init__ of', self)


if __name__ == '__main__':
    print()

    a1 = A(); a2 = A(); print(a1, a2)
    print()

    b1 = B(); b2 = B(); print(b1, b2)

4.2.2. 加鎖版

import threading

_lock = threading.Lock()

def Singleton(cls):
    print("正在進(jìn)行裝飾類", cls, '哦~')

    global _lock
    cls._instance = None

    def _singleton(*args, **kargs):
        if cls._instance: return cls._instance
        _lock.acquire()#上鎖
        cls._instance = cls(*args, **kargs)
        _lock.release()#去鎖
        return cls._instance
    
    return _singleton

@Singleton
class A(object):
    @classmethod
    def __new__(cls, *args, **kwargs):
        print('__new__ of ', cls)
        return super().__new__(cls)

    def __init__(self, *args, **kwargs):
        print('__init__ of', self)
    

@Singleton
class B(object):
    @classmethod
    def __new__(cls, *args, **kwargs):
        print('__new__ of ', cls)
        return super().__new__(cls)

    def __init__(self, *args, **kwargs):
        print('__init__ of', self)


if __name__ == '__main__':
    print()

    a1 = A(); a2 = A(); print(a1, a2)
    print()

    b1 = B(); b2 = B(); print(b1, b2)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末手销,一起剝皮案震驚了整個(gè)濱河市脚草,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌原献,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件埂淮,死亡現(xiàn)場(chǎng)離奇詭異姑隅,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)倔撞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門讲仰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人痪蝇,你說我怎么就攤上這事鄙陡。” “怎么了躏啰?”我有些...
    開封第一講書人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵趁矾,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我给僵,道長(zhǎng)毫捣,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任帝际,我火速辦了婚禮蔓同,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蹲诀。我一直安慰自己斑粱,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開白布脯爪。 她就那樣靜靜地躺著则北,像睡著了一般矿微。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上咒锻,一...
    開封第一講書人閱讀 52,441評(píng)論 1 310
  • 那天冷冗,我揣著相機(jī)與錄音,去河邊找鬼惑艇。 笑死蒿辙,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的滨巴。 我是一名探鬼主播思灌,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼恭取!你這毒婦竟也來了泰偿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤蜈垮,失蹤者是張志新(化名)和其女友劉穎耗跛,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體攒发,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡调塌,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了惠猿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片羔砾。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖偶妖,靈堂內(nèi)的尸體忽然破棺而出姜凄,到底是詐尸還是另有隱情,我是刑警寧澤趾访,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布态秧,位于F島的核電站,受9級(jí)特大地震影響腹缩,放射性物質(zhì)發(fā)生泄漏屿聋。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一藏鹊、第九天 我趴在偏房一處隱蔽的房頂上張望润讥。 院中可真熱鬧,春花似錦盘寡、人聲如沸楚殿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)脆粥。三九已至砌溺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間变隔,已是汗流浹背规伐。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留匣缘,地道東北人猖闪。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像肌厨,于是被迫代替她去往敵國(guó)和親培慌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容