博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
通过 python的 __call__ 函数与元类 实现单例模式
阅读量:5996 次
发布时间:2019-06-20

本文共 3492 字,大约阅读时间需要 11 分钟。

简单一句话,当一个类实现__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》,进行部分修改,同时注释部分为个人理解)。

完整源码:

先定义一个名为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__ runningSpam __new__ runningSpam __init__ runningSingleton __call__ runningTrueSingleton __call__ runningTrue

从运行结果上可以看出,每次尝试实例化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__ runningSpam __new__ runningSingleton __call__ runningSpam __new__ runningTrueSingleton __call__ runningSpam __new__ runningTrue

初看似乎没有什么问题,print(a is b) 和 print(a is c)都返回了True。

 

细心的话,会发现上面的输出结果有个很奇怪的问题,__init__方法是从未被执行!!!

对于此,官方文档是如此说明的

If  does not return an instance of cls, then the new instance’s  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。

转载于:https://www.cnblogs.com/blackmatrix/p/6906023.html

你可能感兴趣的文章
bash大括号参数扩展(Parameter Expansion)
查看>>
JVM初探 -JVM内存模型
查看>>
基于AAA的Easy ×××实验
查看>>
LDA(Latent Dirichlet Allocation)学习笔记
查看>>
rabbitmq简单使用
查看>>
Linux Free命令与cache和buffer的主要区别
查看>>
web.xml 配置说明
查看>>
自动化运维之ansible详解
查看>>
protobuf介绍
查看>>
三招打造企业安全内网
查看>>
SCOM 2012 SP1服务器上安装和配置Veeam MP for VMware
查看>>
apache下的 .htaccess 规则
查看>>
redis客户端操作
查看>>
整理 ubuntu 13.10安装并打开telnet
查看>>
pom总结
查看>>
vivo Hi-Fi+QQ音乐 数字音乐市场的一剂良方
查看>>
Android TelephonyManager类的使用
查看>>
我的友情链接
查看>>
【阿里云新品发布·周刊】第8期:数字化风暴已经来临!云+区块链,如何颠覆未来科技?...
查看>>
linux centos samba服务
查看>>