設(shè)計(jì)模式(Python)-單例模式

本系列文章是希望將軟件項(xiàng)目中最常見的設(shè)計(jì)模式用通俗易懂的語言來講解清楚,并通過Python來實(shí)現(xiàn),每個(gè)設(shè)計(jì)模式都是圍繞如下三個(gè)問題:

  1. 為什么?即為什么要使用這個(gè)設(shè)計(jì)模式,在使用這個(gè)模式之前存在什么樣的問題?
  2. 是什么?通過Python語言來去實(shí)現(xiàn)這個(gè)設(shè)計(jì)模式,用于解決為什么中提到的問題。
  3. 怎么用?理解了為什么我們也就基本了解了什么情況下使用這個(gè)模式,不過在這里還是會(huì)細(xì)化使用場景,闡述模式的局限和優(yōu)缺點(diǎn)。

這一篇我們先來看看單例模式。單例模式是設(shè)計(jì)模式中邏輯最簡單,最容易理解的一個(gè)模式,簡單到只需要一句話就可以理解,即“保證只有一個(gè)對象實(shí)例的模式”。問題的關(guān)鍵在于實(shí)現(xiàn)起來并沒有想象的那么簡單。不過我們還是先來討論下為什么需要這個(gè)模式吧。

為什么

我們首先來看看單例模式的使用場景,然后再來分析為什么需要單例模式。

  • Python的logger就是一個(gè)單例模式,用以日志記錄
  • Windows的資源管理器是一個(gè)單例模式
  • 線程池,數(shù)據(jù)庫連接池等資源池一般也用單例模式
  • 網(wǎng)站計(jì)數(shù)器

從這些使用場景我們可以總結(jié)下什么情況下需要單例模式:

  1. 當(dāng)每個(gè)實(shí)例都會(huì)占用資源,而且實(shí)例初始化會(huì)影響性能,這個(gè)時(shí)候就可以考慮使用單例模式,它給我們帶來的好處是只有一個(gè)實(shí)例占用資源,并且只需初始化一次;
  2. 當(dāng)有同步需要的時(shí)候,可以通過一個(gè)實(shí)例來進(jìn)行同步控制,比如對某個(gè)共享文件(如日志文件)的控制,對計(jì)數(shù)器的同步控制等,這種情況下由于只有一個(gè)實(shí)例,所以不用擔(dān)心同步問題。

當(dāng)然所有使用單例模式的前提是我們的確用一個(gè)實(shí)例就可以搞定要解決的問題,而不需要多個(gè)實(shí)例,如果每個(gè)實(shí)例都需要維護(hù)自己的狀態(tài),這種情況下單例模式肯定是不適用的。
接下來看看如何使用Python來實(shí)現(xiàn)一個(gè)單例模式。

是什么

最開始的想法很簡單,實(shí)現(xiàn)如下:

class Singleton(object):
    __instance = None
    def __new__(cls, *args, **kwargs):  # 這里不能使用__init__,因?yàn)開_init__是在instance已經(jīng)生成以后才去調(diào)用的
        if cls.__instance is None:
            cls.__instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
        return cls.__instance

s1 = Singleton()
s2 = Singleton()
print s1
print s2

打印結(jié)果如下:

<__main__.Singleton object at 0x7f3580dbe110>
<__main__.Singleton object at 0x7f3580dbe110>

可以看出兩次創(chuàng)建對象,結(jié)果返回的是同一個(gè)對象實(shí)例,我們再讓我們的例子更接近真實(shí)的使用場景來看看

class Singleton(object):
    __instance = None

    def __new__(cls, *args, **kwargs):
        if cls.__instance is None:
            cls.__instance = super(
                Singleton, cls).__new__(cls, *args, **kwargs)
        return cls.__instance

    def __init__(self, status_number):
        self.status_number = status_number


s1 = Singleton(2)
s2 = Singleton(5)
print s1
print s2

print s1.status_number
print s2.status_number

這里我們使用了_init_方法,下面是打印結(jié)果,可以看出確實(shí)是只有一個(gè)實(shí)例,共享了實(shí)例的變量

<__main__.Singleton object at 0x7f5116865490>
<__main__.Singleton object at 0x7f5116865490>
5
5

不過這個(gè)例子中有一個(gè)問題我們沒有解決,那就是多線程的問題,當(dāng)有多個(gè)線程同時(shí)去初始化對象時(shí),就很可能同時(shí)判斷__instance is None,從而進(jìn)入初始化instance的代碼中。所以為了解決這個(gè)問題,我們必須通過同步鎖來解決這個(gè)問題。以下例子來自xiaorui

import threading
try:
    from synchronize import make_synchronized
except ImportError:
    def make_synchronized(func):
        import threading
        func.__lock__ = threading.Lock()

        def synced_func(*args, **kws):
            with func.__lock__:
                return func(*args, **kws)

        return synced_func


class Singleton(object):
    instance = None

    @make_synchronized
    def __new__(cls, *args, **kwargs):
        if cls.instance is None:
            cls.instance = object.__new__(cls, *args, **kwargs)
        return cls.instance

    def __init__(self):
        self.blog = "xiaorui.cc"

    def go(self):
        pass


def worker():
    e = Singleton()
    print id(e)
    e.go()


def test():
    e1 = Singleton()
    e2 = Singleton()
    e1.blog = 123
    print e1.blog
    print e2.blog
    print id(e1)
    print id(e2)


if __name__ == "__main__":
    test()
    task = []
    for one in range(30):
        t = threading.Thread(target=worker)
        task.append(t)

    for one in task:
        one.start()

    for one in task:
        one.join()

至此我們的單例模式實(shí)現(xiàn)代碼已經(jīng)接近完美了,不過我們是否可以更簡單地使用單例模式呢?答案是有的,接下來就看看如何更簡單地使用單例模式。

怎么用

在Python的官方網(wǎng)站給了兩個(gè)例子是用裝飾符來修飾類,從而使得類變成了單例模式,使得我們可以通過更加簡單的方式去實(shí)現(xiàn)單例模式
例子:(這里只給出一個(gè)例子,因?yàn)楦唵危硗庖粋€(gè)大家可以看官網(wǎng)Singleton

def singleton(cls):
    instance = cls()
    instance.__call__ = lambda: instance
    return instance

#
# Sample use
#

@singleton
class Highlander:
    x = 100
    # Of course you can have any attributes or methods you like.

Highlander() is Highlander() is Highlander #=> True
id(Highlander()) == id(Highlander) #=> True
Highlander().x == Highlander.x == 100 #=> True
Highlander.x = 50
Highlander().x == Highlander.x == 50 #=> True

這里簡單解釋下:

  1. 在定義class Highlander的時(shí)候已經(jīng)執(zhí)行完所有singleton裝飾器中的代碼,得到了一個(gè)instance,所以這之后所有對Highlander的調(diào)用實(shí)際上是在調(diào)用instance的_call_ 方法。
  2. 我們通過lambda函數(shù)定義了_call_方法讓它始終返回instance,因此Highlander()和Highlander都返回instance
  3. 同時(shí)由于在類定義代碼執(zhí)行時(shí)就已經(jīng)創(chuàng)建了instance,所以后續(xù)不論是多線程還是單線程,在調(diào)用Highlander時(shí)都是在調(diào)用instance的_call_方法,也就無需同步了。
    最后我想說的是這種方法簡直碉堡了~~~
    附上我用于多線程的測試代碼
import threading

def singleton(cls):
    instance = cls()
    instance.__call__ = lambda: instance
    return instance


@singleton
class Highlander:
    x = 100
    # Of course you can have any attributes or methods you like.


def worker():
    hl = Highlander()
    hl.x += 1
    print hl
    print hl.x


def main():
    threads = []
    for _ in xrange(50):
        t = threading.Thread(target=worker)
        threads.append(t)

    for t in threads:
        t.start()

    for t in threads:
        t.join()


if __name__ == '__main__':
    main()

這里的代碼有一點(diǎn)小問題,就是在打印的時(shí)候有可能x屬性已經(jīng)被別的線程+1了,所以有可能導(dǎo)致同一個(gè)數(shù)打印多次,而有的數(shù)沒有打印,但是不影響最終x屬性的結(jié)果,所以當(dāng)所有線程結(jié)束之后,屬性x最終的值是可以保證正確的。

Reference:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容