本系列文章是希望將軟件項目中最常見的設計模式用通俗易懂的語言來講解清楚,并通過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最終的值是可以保證正確的。