专栏首页小陈学PythonPython 设计模式(5):单例模式

Python 设计模式(5):单例模式

导言

在软件设计中,有时确实存在一个类仅能用来产生一个唯一对象的必要性,例如,一个大公司的打印室虽然可以有多台打印机,但是其打印管理系统中只有一个打印任务控制对象,该对象管理打印排队并分配打印任务给各个打印机。再如,在 Windows 系统中,应该只有一个文件系统与一个文件视窗管理系统(Window Manager)。

怎样确保一个类只有一个实例,并且该实例可以容易的获得呢?有两个方法解决该问题,一个是程序员在应用程序中使用代码保证仅有一个实例被创建,另外一个方法是不依靠应用程序员,而是精心设计需要仅有一个实例的类,由该类本身的结构确保其仅能够被创建一个实例。很明显,第一个方法有许多缺点,因为程序员很可能疏忽而导致第二个实例被创建。实践证明,第二个方法是从根本上保证仅有一个实例被创建的有效方法。这就是单例模式(Singleton Pattern)所要表述的内容。

单例模式是指确保一个类仅有一个唯一的实例,并且提供一个全局的访问点。

Java 实现单例模式的思路是,为了防止客户程序利用构造方法创建多个对象,将构造方法声明为 private 类型。其原因是,如果构造方法是 public 类型的,则客户程序永远可以使用该类构造方法创建不同的对象。但这样做的问题是,如果一个类的构造方法是 private 的,则其他类就无法使用该类的构造方法来创建对象,从而该类就成为不可用的。

为了解决这个问题,该类必须提供一个可以获得实例的方法,通常称为 getInstance 方法。该方法返回一个类的实例。

我们可以发现要想实现单例模式,“私有”成了一个关键字。然而,在 Python 中,并没有绝对的私有,撑死只能用两个下划线开头实现伪私有。即使如此,Python 依旧可以实现单例模式,只不过有风险,具体有什么风险,后面再说。我们先实现一下单例模式,Python 实现单例模式最简单的方法是使用模块。把类和该类的一个实例对象单独放在一个模块,然后只需要导入该类的实例即可。刚刚我说有风险,现在大家应该明白为什么有风险了吧?如果我导入的不是实例变量,而是类本身,那不就违背单例模式了吗?这种方法虽然简单,但是有一定的风险,所以我建议换一种方法来实现单例模式。我们先想一下,Python 创建一个对象的过程是怎样的?刚学 Python 的人会认为就是执行 __init__,其实不是,在 __init__ 之前还要执行一个魔法方法 __new__!我们先看一下 __new__ 方法的定义。

    @staticmethod # known case of __new__
    def __new__(cls, *more): # known special case of object.__new__
        """ Create and return a new object.  See help(type) for accurate signature. """
        pass

稍微翻译一下,我们就知道 __new__ 的作用:创建并返回一个新的对象。既然如此,我们就可以使用重写 __new__ 魔法方法来实现单例模式。

class President:
    instance = None

    def __new__(cls, *args, **kwargs):
        if not isinstance(cls.instance, President):
            cls.instance = object.__new__(cls)
        return cls.instance


president1 = President()
president2 = President()
print(president1 is president2)

这段代码运行结果是 True,说明两个变量指向了同一块内存地址,那一块内存地址存放的就是这个类的实例。其实这么写还是有问题,如果我在外部修改静态属性 instance 就可以破坏单例模式的规则了,代码如下:

class President:
    instance = None

    def __new__(cls, *args, **kwargs):
        if not isinstance(cls.instance, President):
            cls.instance = object.__new__(cls)
        return cls.instance


president1 = President()
President.instance = None
president2 = President()
print(president1 is president2)

这样的话运行结果就是 False 了,说明两个变量指向了不同的内存地址,也就是说类被实例化了两次。所以这种方法还是尽量避免吧。其实,在 Python 中,有一个神秘而有强大的神器,它可以动态的修改一个函数和一个类的功能,它就是装饰器!我们可以用它来装饰一个类,使其只能生成一个实例,代码如下:

def singleton(cls):
    instance = {}

    def get_instance(*args, **kwargs):
        if cls not in instance:
            instance[cls] = cls(*args, **kwargs)
        return instance[cls]
    return get_instance


@singleton
class President:
    pass


if __name__ == '__main__':
    president1 = President()
    president2 = President()
    print(president1 is president2)

运行结果为 True,这种方法看上去没有之前的风险了,也实现了单例模式,但是还有一个问题,在多线程的情况下,还能正确的运行吗?我们先尝试定义 10 个线程,每个线程实例化一个对象,代码如下:

from threading import Thread


def singleton(cls):
    instance = {}

    def get_instance(*args, **kwargs):
        if cls not in instance:
            instance[cls] = cls(*args, **kwargs)
        return instance[cls]
    return get_instance


@singleton
class President:
    pass


def target():
    president = President()
    print(president)


if __name__ == '__main__':
    threads = [Thread(target=target)for i in range(10)]
    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join()

运行结果如图所示:

从结果中可以看出每个线程创建的对象指向同一个内存,发现没什么问题,但并不代表没有问题,为什么这么说?是因为Python有一把超级大锁 GIL(Global Interpreter Lock,全局解释器锁),这把锁保证一个时间只有一个线程,除非当前运行的线程遇到阻塞才会有线程切换,那么我们就阻塞一下看看,代码如下:

from threading import Thread
from time import sleep


def singleton(cls):
    instance = {}

    def get_instance(*args, **kwargs):
        if cls not in instance:
            instance[cls] = cls(*args, **kwargs)
        return instance[cls]
    return get_instance


@singleton
class President:
    def __init__(self):
        sleep(1)


def target():
    president = President()
    print(president)


if __name__ == '__main__':
    threads = [Thread(target=target)for i in range(10)]
    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join()

在这里我通过sleep来阻塞线程,目的是看在线程频繁切换的过程中还能不能正确的运行,运行结果如图所示:

从运行结果中可以看出,每个线程创建的对象指向了不同的内存,所以我们还需要给 get_instance 方法做一个同步,这里使用装饰器来做同步,代码如下:

from threading import Thread, Lock
from time import sleep


def synchronized(func):
    lock = Lock()

    def synchronized_func(*args, **kwargs):
        with lock:
            return func(*args, **kwargs)
    return synchronized_func


def singleton(cls):
    instance = {}

    @synchronized
    def get_instance(*args, **kwargs):
        if cls not in instance:
            instance[cls] = cls(*args, **kwargs)
        return instance[cls]
    return get_instance


@singleton
class President:
    def __init__(self):
        sleep(1)


def target():
    president = President()
    print(president)


if __name__ == '__main__':
    threads = [Thread(target=target)for i in range(10)]
    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join()

运行结果如图所示:

不容易啊!费了这么多功夫,终于实现了一个目前看来没有任何问题的单例模式了!

本文分享自微信公众号 - 小陈学Python(gh_a29b1ed16571)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-03-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 让web开发部署提速 8 倍的一款IDEA插件

    在接触 Cloud Toolkit 之前,用什么方法来部署一个 SpringBoot 应用呢?作为一个偏正经的测评人员,我不会为了凸显出 Cloud Toolk...

    lyb-geek
  • linux下jenkins的安装

    有可能出现错误:“Starting Jenkins -bash: /usr/bin/java: No such file or directory”。表示找不到...

    用户1499526
  • 解决QML debugging is enabled.Only use this in a safe environment警告

    Qt君
  • Spring boot学习

    通常建议将应用的main类放到其他类所在包的顶层(root package),并将 @EnableAutoConfiguration 注解到你的mai...

    用户1499526
  • JAVA和tomcat 环境的配置

    在tomca7/bin 目录下面,新建 setenv.sh配置,catalina.sh启动的时候会调用,同时配置java内存参数 setenv.sh的内容如...

    用户1499526
  • Java8内存结构的改变~

    根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。

    Spark学习技巧
  • Databus调研踩坑记录

    解决方式: 下载一个ojdbc6-11.2.0.2.0.jar的jar包放到/Users/wenba/Desktop/tools/databus/databus...

    用户2825413
  • 一文搞懂springboot启动原理

    从上面代码可以看出,Annotation定义(@SpringBootApplication)和类定义(SpringApplication.run)最为耀眼,所以...

    lyb-geek
  • 这些Spring中的设计模式,你都知道吗?

    设计模式作为工作学习中的枕边书,却时常处于勤说不用的尴尬境地,也不是我们时常忘记,只是一直没有记忆。

    黄泽杰
  • 面试题:你简历中写到熟悉Spring源码,那你给我说说它用到了那些设计模式?

    spring的jdbc模板,对Spring源码的精妙真是佩服得五体投地,极为经典。

    lyb-geek

扫码关注云+社区

领取腾讯云代金券