本系列文章是希望將軟件項(xiàng)目中最常見的設(shè)計(jì)模式用通俗易懂的語言來講解清楚,并通過Python來實(shí)現(xiàn),每個(gè)設(shè)計(jì)模式都是圍繞如下三個(gè)問題:
- 為什么?即為什么要使用這個(gè)設(shè)計(jì)模式,在使用這個(gè)模式之前存在什么樣的問題?
- 是什么?通過Python語言來去實(shí)現(xiàn)這個(gè)設(shè)計(jì)模式,用于解決為什么中提到的問題。
- 怎么用?理解了為什么我們也就基本了解了什么情況下使用這個(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é)下什么情況下需要單例模式:
- 當(dāng)每個(gè)實(shí)例都會(huì)占用資源,而且實(shí)例初始化會(huì)影響性能,這個(gè)時(shí)候就可以考慮使用單例模式,它給我們帶來的好處是只有一個(gè)實(shí)例占用資源,并且只需初始化一次;
- 當(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
這里簡單解釋下:
- 在定義class Highlander的時(shí)候已經(jīng)執(zhí)行完所有singleton裝飾器中的代碼,得到了一個(gè)instance,所以這之后所有對Highlander的調(diào)用實(shí)際上是在調(diào)用instance的_call_ 方法。
- 我們通過lambda函數(shù)定義了_call_方法讓它始終返回instance,因此Highlander()和Highlander都返回instance
- 同時(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最終的值是可以保證正確的。