Python詳解設計模式:單例模式

前段時間,有朋友在個人讀者羣裏問了幾個關於單例模式的問題。python

爲了回答他的問題,我整理了單例模式的知識點,正好我也在寫設計模式的系列。設計模式

上一篇是講「策略模式」,若你還未閱讀,能夠點擊這裏查看:安全

【經典案例】Python詳解設計模式:策略模式併發

本篇作爲「設計模式系列」的第二篇,來一塊兒看看「單例模式」。分佈式

以前在另外一篇公衆號文章看到一個挺搞笑的例子:函數

大意是講,老婆在中國其實就是一個很形象的單例,你要娶一個老婆須要去民政局註冊登記(要對類進行實例化),當你想再娶一個老婆時,這時民政局會說,不行,你已經有一個老婆了,而且它還會告訴你的老婆是誰。 而後有個朋友,還很生趣地評論說post

單例模式 容許你討無數個老婆,但最終你會發現你討來的老婆都是同一我的測試

玩笑以後,再回到咱們的話題,先舉幾類咱們常常見到的例子:網站

一、你們在解釋單例模式時,常常要提到的一個例子是 Windows 的任務管理器。若是咱們打開多個任務管理器窗口。顯示的內容徹底一致,若是在內部是兩個如出一轍的對象,那就是重複對象,就形成了內存的浪費;相反,若是兩個窗口的內容不一致,那就會至少有一個窗口展現的內容是錯誤的,會給用戶形成誤解,到底哪一個纔是當前真實的狀態呢?spa

二、一個項目中多個地方須要讀取同一份配置文件,若是每次使用都是導入從新建立實例,讀取文件,用完後再銷燬,這樣作的話,就形成沒必要要的IO浪費,可使用單例模式只生成一份配置在內存中。

三、還有一個常見的例子是,一個網站的訪問量、在線人數,在項目中是全局惟一(不考慮分佈式),在這種狀況下,使用單例模式是一種很好的方式。

從上面看來,在系統中確保某個對象的惟一性即一個類只能有一個實例有時是很是重要的。

按照慣例,咱們先來用代碼實踐一下,看看如何用 Python 寫單例模式。

這裏介紹了三個較爲經常使用的。

  • 使用 __new__
class User:
	_instance = None
	def __new__(cls, *args, **kwargs):
		print('===== 1 ====')
		if not cls._instance:
			print("===== 2 ====")
			cls._instance = super().__new__(cls)
		return cls._instance

	def __init__(self, name):
		print('===== 3 ====')
		self.name = name
複製代碼

驗證結果

  • 使用裝飾器
instances = {}

def singleton(cls):
	def get_instance(*args, **kw):
		cls_name = cls.__name__
		print('===== 1 ====')
		if not cls_name in instances:
			print('===== 2 ====')
			instance = cls(*args, **kw)
			instances[cls_name] = instance
		return instances[cls_name]
	return get_instance

@singleton
class User:
	_instance = None

	def __init__(self, name):
		print('===== 3 ====')
		self.name = name
複製代碼

驗證結果

  • 使用元類
class MetaSingleton(type):
	def __call__(cls, *args, **kwargs):
		print("cls:{}".format(cls.__name__))
		print("====1====")
		if not hasattr(cls, "_instance"):
			print("====2====")
			cls._instance = type.__call__(cls, *args, **kwargs)
		return cls._instance

class User(metaclass=MetaSingleton):
	def __init__(self, *args, **kw):
		print("====3====")
		for k,v in kw:
			setattr(self, k, v)
複製代碼

驗證結果

以上的代碼,通常狀況下沒有問題,但在併發場景中,就會出現線程安全的問題。

以下這段代碼我開啓10個線程來模擬

import time
import threading

class User:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            time.sleep(1)
            cls._instance = super().__new__(cls)
        return cls._instance

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

def task():
    u = User("wangbm")
    print(u)

for i in range(10):
    t = threading.Thread(target=task)
    t.start()
複製代碼

從結果來觀察,很容易就發現,單例模式失效了,在10個線程下,併發建立實例,並不能保證一個類只有一個實例。

<__main__.User object at 0x1050563c8>
<__main__.User object at 0x10551a208>
<__main__.User object at 0x1050563c8>
<__main__.User object at 0x1055a93c8>
<__main__.User object at 0x1050563c8>
<__main__.User object at 0x105527160>
<__main__.User object at 0x1055f4e48>
<__main__.User object at 0x1055e6c88>
<__main__.User object at 0x1055afcf8>
<__main__.User object at 0x105605940>
複製代碼

這在 Java 中,是可使用餓漢模式來避免這個問題,在 Python 中我想到的辦法是加鎖

首先實現一個給函數加鎖的裝飾器

import threading

def synchronized(func):

    func.__lock__ = threading.Lock()

    def lock_func(*args, **kwargs):
        with func.__lock__:
            return func(*args, **kwargs)
    return lock_func
複製代碼

而後在實例化對象的函數上,使用這個裝飾函數。

import time
import threading

class User:
    _instance = None

 @synchronized
    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            time.sleep(1)
            cls._instance = super().__new__(cls)
        return cls._instance

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

def task():
    u = User("wangbm")
    print(u)

for i in range(10):
    t = threading.Thread(target=task)
    t.start()
複製代碼

結果以下,如預期只生成了一個實例。

<__main__.User object at 0x10ff503c8>
<__main__.User object at 0x10ff503c8>
<__main__.User object at 0x10ff503c8>
<__main__.User object at 0x10ff503c8>
<__main__.User object at 0x10ff503c8>
<__main__.User object at 0x10ff503c8>
<__main__.User object at 0x10ff503c8>
<__main__.User object at 0x10ff503c8>
<__main__.User object at 0x10ff503c8>
<__main__.User object at 0x10ff503c8>
複製代碼

學會寫只是第一步,還有一點,至關重要,要知道爲什麼會有這個設計模式,它有什麼優點,有什麼侷限性?

總結一下,單例模式有以下優勢:

  1. 全局只有一個接入點,能夠更好地進行數據同步控制,避免多重佔用;
  2. 因爲單例模式要求在全局內只有一個實例,於是能夠節省比較多的內存空間;
  3. 單例可長駐內存,減小系統開銷。

和其餘設計模式同樣,單例模式有必定的適用場景,但同時它也會給咱們帶來一些問題。

  1. 因爲單例對象是全局共享,因此其狀態維護須要特別當心。一處修改,全局都會受到影響。
  2. 單例對象沒有抽象層,擴展不便。
  3. 賦於了單例以太多的職責,某種程度上違反單一職責原則(六大原則後面會講到);
  4. 單例模式是併發協做軟件模塊中須要最早完成的,於是其不利於測試;
  5. 單例模式在某種狀況下會致使「資源瓶頸」。

參考文章


關注公衆號,獲取最新干貨!
相關文章
相關標籤/搜索