深度解密Python單例模式

  1. 認識單例模式
    認識單例模式

1 單例模式含義
2 單例模式優點
3 單例模式缺點
4 單例模式應用

  1. Python實現單例模式
    Python實現單例模式

1 多種實現方法
2 實例分析

  1. 總結
    總結

認識單例模式
1.1 單例模式含義
單例模式,也叫單子模式,是一種常用的軟件設計模式。在應用這個模式時,單例對象的類必須保證只有一個實例存在。許多時候整個系統只需要擁有一個的全局對象,這樣有利於我們協調系統整體的行爲。比如在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,然後服務進程中的其他對象再通過這個單例對象獲取這些配置信息。這種方式簡化了在複雜環境下的配置管理。

實現單例模式的思路是:一個類能返回對象一個引用(永遠是同一個)和一個獲得該實例的方法(必須是靜態方法,通常使用getInstance這個名稱);當我們調用這個方法時,如果類持有的引用不爲空就返回這個引用,如果類保持的引用爲空就創建該類的實例並將實例的引用賦予該類保持的引用;同時我們還將該類的構造函數定義爲私有方法,這樣其他處的代碼就無法通過調用該類的構造函數來實例化該類的對象,只有通過該類提供的靜態方法來得到該類的唯一實例。

單例模式在多線程的應用場合下必須小心使用。如果當唯一實例尚未創建時,有兩個線程同時調用創建方法,那麼它們同時沒有檢測到唯一實例的存在,從而同時各自創建了一個實例,這樣就有兩個實例被構造出來,從而違反了單例模式中實例唯一的原則。 解決這個問題的辦法是爲指示類是否已經實例化的變量提供一個互斥鎖(雖然這樣會降低效率)。

1.2 單例模式優點
單例模式的優點:
1、由於單例模式要求在全局內只有一個實例,因而可以節省比較多的內存空間;
2、全局只有一個接入點,可以更好地進行數據同步控制,避免多重佔用;
3、單例可長駐內存,減少系統開銷。

1.3 單例模式缺點
單例模式的缺點
1、單例模式的擴展是比較困難的;
2、賦於了單例以太多的職責,某種程度上違反單一職責原則(六大原則後面會講到);
3、單例模式是併發協作軟件模塊中需要最先完成的,因而其不利於測試;
4、單例模式在某種情況下會導致“資源瓶頸”。

1.4 單例模式應用
單例模式的應用舉例:
1、生成全局惟一的序列號;
2、訪問全局複用的惟一資源,如磁盤、總線等;
3、單個對象佔用的資源過多,如數據庫等;
4、系統全局統一管理,如Windows下的Task Manager;
5、網站計數器。

Python實現單例模式
2.1 多種實現方法
2.1.1.使用模塊
其實,Python 的模塊就是天然的單例模式,因爲模塊在第一次導入時,會生成 .pyc 文件,當第二次導入時,就會直接加載 .pyc 文件,而不會再次執行模塊代碼。因此,我們只需把相關的函數和數據定義在一個模塊中,就可以獲得一個單例對象了。如果我們真的想要一個單例類,可以考慮這樣做:

singleton_by_module.py

class Singleton(object):

def foo(self):
    pass

singleton = Singleton()
將上面的代碼保存在文件 singleton_by_module.py 中,要使用時,直接在其他文件中導入此文件中的對象,這個對象即是單例模式的對象
test_singleton_by_module.py

from singleton_by_module import Singleton

t = Singleton()
這樣我們一旦調用到singleton_by_module.py就會產生一個singleton_by_module.pyc,以後我們每次調用都會直接引用這裏面的代碼。

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
根據上面的運行情況,我們可以發現,當a1被創建後調用的是正常的產生實例的過程,當a2被創建的時候,由於之前實例已經被存儲下來,所以直接引用了a1的實例,所以他們的id是一樣的,也就是他們引用了同一個內存實例。

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
一般情況,大家以爲這樣就完成了單例模式,但是這樣當使用多線程時會存在問題

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()

程序執行後,打印結果如下:

<__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>
看起來也沒有問題,那是因爲執行速度過快,如果在init方法中有一些IO操作,就會發現問題了,下面我們通過time.sleep模擬

我們在上面init方法中加入以下代碼:

singleton_by_class_mutli_threading_sleep.py

def __init__(self):

    import time
    time.sleep(1)

重新執行程序後,結果如下

<__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>
問題出現了!按照以上方式創建的單例,無法支持多線程

解決辦法:加鎖!未加鎖部分併發執行,加鎖部分串行執行,速度降低,但是保證了數據安全

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)
打印結果如下:

<__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>
這樣就差不多了,但是還是有一點小問題,就是當程序執行時,執行了time.sleep(20)後,下面實例化對象時,此時已經是單例模式了,但我們還是加了鎖,這樣不太好,再進行一些優化,把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)

完整代碼
這種方式實現的單例模式,使用時會有限制,以後實例化必須通過 obj = Singleton.instance()

如果用 obj=Singleton() ,這種方式得到的不是單例

2.1.4基於new方法實現(推薦使用,方便)
通過上面例子,我們可以知道,當我們實現單例時,爲了保證線程安全需要在內部加入鎖

我們知道,當我們實例化一個對象時,是先執行了類的new方法(我們沒寫時,默認調用type.new),實例化對象;然後再執行類的init方法,對這個對象進行初始化,所有我們可以基於這個,實現單例模式

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()

打印結果如下:

<__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>
採用這種方式的單例模式,以後實例化對象時,和平時實例化對象的方法一樣 obj = Singleton()

2.1.5.基於metaclass方式實現
相關知識

"""
1.類由type創建,創建類時,type的init方法自動執行,類() 執行type的 call方法(類的new方法,類的init方法)
2.對象由類創建,創建對象時,類的init方法自動執行,對象()執行類的 call 方法
"""

class Foo:

def __init__(self):
    pass

def __call__(self, *args, **kwargs):
    pass

obj = Foo()

執行type的 call 方法,調用 Foo類(是type的對象)的 __new__方法,用於創建對象,然後調用 Foo類(是type的對象)的 __init__方法,用於對對象初始化。

obj() # 執行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): # 指定創建Foo的type爲SingletonType

def __init__(self,name):
    self.name = name
def __new__(cls, *args, **kwargs):
    return object.__new__(cls)

obj = Foo('xx')
實現單例模式

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 實例分析
總線是計算機各種功能部件或者設備之間傳送數據、控制信號等信息的公共通信解決方案之一。現假設有如下場景:某中央處理器(CPU)通過某種協議總線與一個信號燈相連,信號燈有64種顏色可以設置,中央處理器上運行着三個線程,都可以對這個信號燈進行控制,並且可以獨立設置該信號燈的顏色。抽象掉協議細節(用打印表示),如何實現線程對信號等的控制邏輯。
加線程鎖進行控制,無疑是最先想到的方法,但各個線程對鎖的控制,無疑加大了模塊之間的耦合。下面,我們就用設計模式中的單例模式,來解決這個問題。

代碼如下:

import threading
import time

這裏使用方法__new__來實現單例模式

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對象實例化寫在了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()

運行結果如下:
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
在程序運行過程中,三個線程同時運行(運行結果的前三行先很快打印出來),而後分別佔用總線資源(後三行每隔3秒打印一行)。雖然看上去總線Bus被實例化了三次,但實際上在內存裏只有一個實例。

總結
因爲單例模式在設計模式中算是最基礎且最簡單的一個模式,因此在一般初級面試的時候,面試官都會通過這個問題來考察,一個很重要的原因是單例模式實現方法多種且優化的方式也有很多,所以也很能考察應聘者的水平,所以,大家要好好學這個最基礎的設計模式啊!另外,在Java中單例模式常說的飽漢餓漢模式,其實和Python中的利用__new__和利用class來創建是一樣的,也就是在什麼時候創建實例的區別。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章