本系列文章是希望將軟件項目中最常見的設計模式用通俗易懂的語言來講解清楚,並通過Python來實現,每個設計模式都是圍繞如下三個問題:
- 為什么?即為什么要使用這個設計模式,在使用這個模式之前存在什么樣的問題?
- 是什么?通過Python語言來去實現這個設計模式,用於解決為什么中提到的問題。
- 怎么用?理解了為什么我們也就基本了解了什么情況下使用這個模式,不過在這里還是會細化使用場景,闡述模式的局限和優缺點。
這一篇我們先來看看單例模式。單例模式是設計模式中邏輯最簡單,最容易理解的一個模式,簡單到只需要一句話就可以理解,即“保證只有一個對象實例的模式”。問題的關鍵在於實現起來並沒有想象的那么簡單。不過我們還是先來討論下為什么需要這個模式吧。
為什么
我們首先來看看單例模式的使用場景,然后再來分析為什么需要單例模式。
- Python的logger就是一個單例模式,用以日志記錄
- Windows的資源管理器是一個單例模式
- 線程池,數據庫連接池等資源池一般也用單例模式
- 網站計數器
從這些使用場景我們可以總結下什么情況下需要單例模式:
- 當每個實例都會占用資源,而且實例初始化會影響性能,這個時候就可以考慮使用單例模式,它給我們帶來的好處是只有一個實例占用資源,並且只需初始化一次;
- 當有同步需要的時候,可以通過一個實例來進行同步控制,比如對某個共享文件(如日志文件)的控制,對計數器的同步控制等,這種情況下由於只有一個實例,所以不用擔心同步問題。
當然所有使用單例模式的前提是我們的確用一個實例就可以搞定要解決的問題,而不需要多個實例,如果每個實例都需要維護自己的狀態,這種情況下單例模式肯定是不適用的。
接下來看看如何使用Python來實現一個單例模式。
是什么
最開始的想法很簡單,實現如下:
class Singleton(object): __instance = None def __new__(cls, *args, **kwargs): # 這里不能使用__init__,因為__init__是在instance已經生成以后才去調用的 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
打印結果如下:
<__main__.Singleton object at 0x7f3580dbe110>
<__main__.Singleton object at 0x7f3580dbe110>
可以看出兩次創建對象,結果返回的是同一個對象實例,我們再讓我們的例子更接近真實的使用場景來看看
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_方法,下面是打印結果,可以看出確實是只有一個實例,共享了實例的變量
<__main__.Singleton object at 0x7f5116865490>
<__main__.Singleton object at 0x7f5116865490>
5
5
不過這個例子中有一個問題我們沒有解決,那就是多線程的問題,當有多個線程同時去初始化對象時,就很可能同時判斷__instance is None,從而進入初始化instance的代碼中。所以為了解決這個問題,我們必須通過同步鎖來解決這個問題。以下例子來自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()
至此我們的單例模式實現代碼已經接近完美了,不過我們是否可以更簡單地使用單例模式呢?答案是有的,接下來就看看如何更簡單地使用單例模式。
怎么用
在Python的官方網站給了兩個例子是用裝飾符來修飾類,從而使得類變成了單例模式,使得我們可以通過更加簡單的方式去實現單例模式
例子:(這里只給出一個例子,因為更簡單,另外一個大家可以看官網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
這里簡單解釋下:
- 在定義class Highlander的時候已經執行完所有singleton裝飾器中的代碼,得到了一個instance,所以這之后所有對Highlander的調用實際上是在調用instance的_call_ 方法。
- 我們通過lambda函數定義了_call_方法讓它始終返回instance,因此Highlander()和Highlander都返回instance
- 同時由於在類定義代碼執行時就已經創建了instance,所以后續不論是多線程還是單線程,在調用Highlander時都是在調用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()
這里的代碼有一點小問題,就是在打印的時候有可能x屬性已經被別的線程+1了,所以有可能導致同一個數打印多次,而有的數沒有打印,但是不影響最終x屬性的結果,所以當所有線程結束之后,屬性x最終的值是可以保證正確的。