zoukankan      html  css  js  c++  java
  • 通过 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__,可以在不使用工厂函数的情况,轻松实现单例模式,同时保持不错的可读性。

    (以下代码来自《Python Cookbook》,进行部分修改,同时注释部分为个人理解)。

    完整源码:https://github.com/blackmatrix7/python-learning/blob/master/class_/singleton.py

    先定义一个名为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。

  • 相关阅读:
    注册表开启或关闭ie浏览器-局域网设置-自动监测设置
    关于win32接口文档位置
    网站添加天气预报插件
    json
    re正则
    配置文件
    logging日志模块
    hashlib加密
    sys模块
    os模块
  • 原文地址:https://www.cnblogs.com/blackmatrix/p/6906023.html
Copyright © 2011-2022 走看看