1. 程式人生 > >通過 python的 __call__ 函式與元類 實現單例模式

通過 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。