前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Python 设计模式(5):单例模式

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

作者头像
不可言诉的深渊
发布2019-07-26 17:34:57
5060
发布2019-07-26 17:34:57
举报

导言

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

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

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

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

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

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

代码语言:javascript
复制
    @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__ 魔法方法来实现单例模式。

代码语言:javascript
复制
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 就可以破坏单例模式的规则了,代码如下:

代码语言:javascript
复制
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 中,有一个神秘而有强大的神器,它可以动态的修改一个函数和一个类的功能,它就是装饰器!我们可以用它来装饰一个类,使其只能生成一个实例,代码如下:

代码语言:javascript
复制
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 个线程,每个线程实例化一个对象,代码如下:

代码语言:javascript
复制
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,全局解释器锁),这把锁保证一个时间只有一个线程,除非当前运行的线程遇到阻塞才会有线程切换,那么我们就阻塞一下看看,代码如下:

代码语言:javascript
复制
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 方法做一个同步,这里使用装饰器来做同步,代码如下:

代码语言:javascript
复制
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()

运行结果如图所示:

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-03-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Python机器学习算法说书人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档