通過 python的 __call__ 函式與元類 實現單例模式
簡單一句話,當一個類實現__call__方法時,這個類的例項就會變成可呼叫物件。
直接上測試程式碼
class ClassA: def __call__(self, *args, **kwargs): print('call ClassA instance') if __name__ == '__main__': # ClassA實現了__call__方法 a = ClassA() ''' 這個時候,ClassA的例項a,就變成可呼叫物件 呼叫a(),輸出call ClassA instance,說明是呼叫了 __call__函式 ''' a() # 其實a()等同於a.__call__(),它本質上就是後者的縮寫 a.__call__() # 判斷是否可呼叫,輸出True print(callable(a))
注意,是這個類的例項變成可呼叫物件,類的例項變成可呼叫物件,類的例項變成可呼叫物件,而不是改變這個類的例項化行為。
那麼,如果要改變一個類的被例項化行為呢?
當然要用上黑魔法元類了,因為類本身就是元類的例項,當我們在元類中定義__call__的函式時,會改變類的例項化行為(或者說被呼叫的行為?感覺類和函式的界限有些模糊了)。
利用元類和__call__,可以在不使用工廠函式的情況,輕鬆實現單例模式,同時保持不錯的可讀性。
先定義一個名為Singleton的元類,實現如下
class Singleton(type): def __init__(cls, *args, **kwargs): cls.__instance = None super().__init__(*args, **kwargs) # __call__ 是對於類例項有效,比如說Spam類,是type類的例項 def __call__(cls, *args, **kwargs): print('Singleton __call__ running') if cls.__instance is None: ''' 元類定義__call__方法,可以搶在類執行 __new__ 和 __init__ 之前執行, 也就是建立單例模式的前提,在類例項化前攔截掉。 type的__call__實際上是呼叫了type的__new__和__init__ ''' cls.__instance = super().__call__(*args, **kwargs) return cls.__instance else: return cls.__instance
使用元類建立類Spam
class Spam(metaclass=Singleton): def __new__(cls): print('Spam __new__ running') return super().__new__(cls) def __init__(self): print('Spam __init__ running')
解釋下上面的程式碼,在元類Singleton的__init__函式中,給類增加了一個叫__instance的類屬性。
需要注意的是,__init__的第一個引數是cls,其實等同於我們平時在類中定義__init__的self,因為對於元類來說,類是它的例項。
之所以寫成cls,是便於理解 cls.__instance = None 是給類屬性 __instance 賦值為 None。
接著在元類Singleton中重寫__call__方法,__call__會搶在類(元類的例項)執行__new__和__init__之前執行,也就為攔截類的例項化提供了可能。
在元類Singleton的__call__方法對類屬性__instance進行判斷,如果__instance為None,說明類還未進行例項化,那麼呼叫元類的父類(元類是type的子類)type的__call__方法,同時賦值給 cls.__instance。如果 cls.__instance 不為None,說明類已經進行過例項化,直接返回之前儲存在類屬性cls.__instance 中的類例項,即實現單例模式。
測試程式碼:
if __name__ == '__main__': a = Spam() b = Spam() print(a is b) c = Spam() print(a is c)
執行結果:
Singleton __call__ running Spam __new__ running Spam __init__ running Singleton __call__ running True Singleton __call__ running True
從執行結果上可以看出,每次嘗試例項化Spam時,會被__call__函式攔截,所以會打印出:Singleton __call__ running
接著判斷例項是否存在,在第一次執行時,例項不存在,建立類例項,並賦值給類屬性__instance。
後續的幾次嘗試例項化Spam,因為Spam已經有例項存在,不在建立例項,實現了單例模式。
一個錯誤的例子
如果,我們把Spam的__new__改成下面這樣,不返回任何結果,會有什麼問題??
class Spam(metaclass=Singleton): def __new__(cls): print('Spam __new__ running') def __init__(self): print('Spam __init__ running')
還是執行之前的測試程式碼,得到下面的執行結果
Singleton __call__ running Spam __new__ running Singleton __call__ running Spam __new__ running True Singleton __call__ running Spam __new__ running True
初看似乎沒有什麼問題,print(a is b) 和 print(a is c)都返回了True。
細心的話,會發現上面的輸出結果有個很奇怪的問題,__init__方法是從未被執行!!!
對於此,官方文件是如此說明的
If
__new__()
does not return an instance of cls, then the new instance’s__init__()
method will not be invoked.
當__new__函式沒有返回這個類的例項時,__init__函式不會被呼叫。上面的示例函式,只是列印了文字,沒有返回任何結果,所以不會執行__init__。
其實也容易理解,__init__需要類例項self引數,而__new__沒有返回一個類例項,這樣的話__init__自然無法執行。
但是,還有個問題,為什麼每次Spam的__new__方法都會被執行??
因為 __new__ 不返回任何結果,那麼__init__方法不會執行,例項從頭到尾都沒有被建立過(cls.__instance永遠是None)。
在 執行 a = Spam() b = Spam() c = Spam()的過程中,每次cls.__instance為None,就呼叫type.__call__試圖建立例項。
而每次執行到Spam的__new__方法時,會沒有創建出任何例項,這樣每次都不能成功建立例項。
所以,會有每次都執行Spam __new__ running的情況。
最後,因為 a、b、c 三個例項都是None,所以在做比較時,永遠的True。