很多初學者喜歡用全域性變數,因為這比函式的引數傳來傳去更容易讓人理解。確實在很多場景下用全域性變數很方便。不過如果程式碼規模增大,並且有多個檔案的時候,全域性變數就會變得比較混亂。你可能不知道在哪個檔案中定義了相同型別甚至重名的全域性變數,也不知道這個變數在程式的某個地方被做了怎樣的操作。
因此對於這種情況,有種更好的實現方式:
單例(Singleton)
單例是一種設計模式,應用該模式的類只會生成一個例項。
單例模式保證了在程式的不同位置都可以且僅可以取到同一個物件例項:如果例項不存在,會建立一個例項;如果已存在就會返回這個例項。因為單例是一個類,所以你也可以為其提供相應的操作方法,以便於對這個例項進行管理。
舉個例子來說,比如你開發一款遊戲軟體,遊戲中需要有“場景管理器”這樣一種東西,用來管理遊戲場景的切換、資源載入、網路連線等等任務。這個管理器需要有多種方法和屬性,在程式碼中很多地方會被呼叫,且被呼叫的必須是同一個管理器,否則既容易產生衝突,也會浪費資源。這種情況下,單例模式就是一個很好的實現方法。
單例模式廣泛應用於各種開發場景,對於開發者而言是必須掌握的知識點,同時在很多面試中,也是常見問題。本篇文章總結了目前主流的實現單例模式的方法供讀者參考。
希望看過此文的同學,在以後被面到此問題時,能直接皮一下面試官,“我會 4 種單例模式實現,你想聽哪一種?”
以下是實現方法索引:
- 使用函式裝飾器實現單例
- 使用類裝飾器實現單例
- 使用 __new__ 關鍵字實現單例
- 使用 metaclass 實現單例
使用函式裝飾器實現單例
以下是實現程式碼:
def singleton(cls):
_instance = {}
<span class="k">def</span> <span class="nf">inner</span><span class="p">():</span>
<span class="k">if</span> <span class="n">cls</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">_instance</span><span class="p">:</span>
<span class="n">_instance</span><span class="p">[</span><span class="n">cls</span><span class="p">]</span> <span class="o">=</span> <span class="n">cls</span><span class="p">()</span>
<span class="k">return</span> <span class="n">_instance</span><span class="p">[</span><span class="n">cls</span><span class="p">]</span>
<span class="k">return</span> <span class="n">inner</span>
複製程式碼
@singleton
class Cls(object):
def init(self):
pass
複製程式碼
cls1 = Cls()
cls2 = Cls()
print(id(cls1) == id(cls2))
複製程式碼
輸出結果:
True
複製程式碼
在 Python 中,id 關鍵字可用來檢視物件在記憶體中的存放位置,這裡 cls1 和 cls2 的 id 值相同,說明他們指向了同一個物件。
關於裝飾器的知識,有不明白的同學可以檢視之前的文章 【程式設計課堂】裝飾器淺析 或者使用搜尋引擎再學習一遍。程式碼中比較巧妙的一點是:
_instance = {}
複製程式碼
使用不可變的類地址作為鍵,其例項作為值,每次創造例項時,首先檢視該類是否存在例項,存在的話直接返回該例項即可,否則新建一個例項並存放在字典中。
使用類裝飾器實現單例
程式碼:
class Singleton(object):
def init(self, cls):
self._cls = cls
self._instance = {}
def call(self):
if self._cls not in self._instance:
self._instance[self._cls] = self._cls()
return self._instance[self._cls]
@Singleton
class Cls2(object):
def init(self):
pass
複製程式碼
cls1 = Cls2()
cls2 = Cls2()
print(id(cls1) == id(cls2))
複製程式碼
同時,由於是面對物件的,這裡還可以這麼用
class Cls3():
pass
複製程式碼
Cls3 = Singleton(Cls3)
cls3 = Cls3()
cls4 = Cls3()
print(id(cls3) == id(cls4))
複製程式碼
使用 類裝飾器實現單例的原理和 函式裝飾器 實現的原理相似,理解了上文,再理解這裡應該不難。
New、Metaclass 關鍵字
在接著說另外兩種方法之前,需要了解在 Python 中一個類和一個例項是通過哪些方法以怎樣的順序被創造的。
簡單來說,元類(metaclass) 可以通過方法 metaclass 創造了類(class),而類(class)通過方法 new 創造了例項(instance)。
在單例模式應用中,在創造類的過程中或者創造例項的過程中稍加控制達到最後產生的例項都是一個物件的目的。
本文主講單例模式,所以對這個 topic 只會點到為止,有感興趣的同學可以在網上搜尋相關內容,幾篇參考文章:
- What are metaclasses in Python?
https://stackoverflow.com/questions/100003/what-are-metaclasses-in-python - python-new-magic-method-explained
http://howto.lintel.in/python-new-magic-method-explained/ - Why is init() always called after new()?
https://stackoverflow.com/questions/674304/why-is-init-always-called-after-new
使用 new 關鍵字實現單例模式
使用 new 方法在創造例項時進行干預,達到實現單例模式的目的。
class Single(object):
_instance = None
def new(cls, args, **kw):
if cls._instance is None:
cls._instance = object.new(cls, args, **kw)
return cls._instance
def init(self):
pass
複製程式碼
single1 = Single()
single2 = Single()
print(id(single1) == id(single2))
複製程式碼
在理解到 new 的應用後,理解單例就不難了,這裡使用了
_instance = None
複製程式碼
來存放例項,如果 _instance 為 None,則新建例項,否則直接返回 _instance 存放的例項。
使用 metaclass 實現單例模式
同樣,我們在類的建立時進行干預,從而達到實現單例的目的。
在實現單例之前,需要了解使用 type 創造類的方法,程式碼如下:
def func(self):
print("do sth")
Klass = type("Klass", (), {"func": func})
複製程式碼
c = Klass()
c.func()
複製程式碼
以上,我們使用 type 創造了一個類出來。這裡的知識是 mataclass 實現單例的基礎。
class Singleton(type):
_instances = {}
def call(cls, args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).call(args, **kwargs)
return cls._instances[cls]
class Cls4(metaclass=Singleton):
pass
複製程式碼
cls1 = Cls4()
cls2 = Cls4()
print(id(cls1) == id(cls2))
複製程式碼
這裡,我們將 metaclass 指向 Singleton 類,讓 Singleton 中的 type 來創造新的 Cls4 例項。
小結
本文雖然是講單例模式,但在實現單例模式的過程中,涉及到了蠻多高階 Python 語法,包括裝飾器、元類、new、type 甚至 super 等等。對於新手同學可能難以理解,其實在工程專案中並不需要你掌握的面面俱到,掌握其中一種,剩下的作為了解即可。
by 周鑫鑫
關於更多的設計模式,給初學者推薦《Head First 設計模式》(Head First Design Patterns),此書淺顯易懂,在 Head First 系列書籍裡面也算是很好的一本。
我們的資源網盤裡有電子版,獲取地址請在公眾號(Crossin的程式設計教室)裡回覆關鍵字:資源
════
其他文章及回答:
如何自學Python | 新手引導 | 精選Python問答 | Python單詞表 | 區塊鏈 | 人工智慧 | 雙11 | 嘻哈 | 爬蟲 | 排序演算法
歡迎搜尋及關注:Crossin的程式設計教室