【进阶Python】第六讲:单例模式的妙用

前言

第三讲:类的特殊方法(上篇)中我在讲解Python特殊方法__new__的使用时提及了一个概念--单例模式,这是一个软件设计中非常重要的概念,由于它不属于某一类特定的语言,既可以用于Java、也可以用于Python,因此在这些单一编程语言的书籍里很少特意花费篇幅介绍单例模式,因此,我准备用这整篇文章来介绍一下Python的单例模式的实现及使用场景。

本文,我将从如下3个方面阐述Python单例模式的使用,

  • 单例模式的概念
  • Python单例模式的实现
  • 单例模式的使用场景

单例模式

首先看一下维基百科对单例模式的解释,

单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

上述描述也许有点让人云里雾里,我来提炼一下维基百科关于单例模式解释的关键点,

  1. 单例模式是一种软件设计模式,而不是专属于某种编程语言的语法;
  2. 单例模式只有一个实例存在;
  3. 单例模式有助于协调系统的整体性、统一性;

软件设计模式

我一直认为,对于一门编程语言“入门容易,精通不易”,哪怕是对于很多人都认为简单的Python语言。

我们学会一门语言的基本语法和基本使用也许只需要2个月、2个周,甚至2天或者2个小时,但是如果用一门编程语言开发出高性能的系统,却是一件日积月累的事情。

当使用一门编程语言时一定要认清一个问题,代码不仅是给机器看的,同时也要给人看。因此,我们实现一个工程项目,要同时兼顾代码的高效性和简洁易读性。在效率方面我们可以借助分而治之、动态规划、二叉树、B-树等算法设计模式和数据结构,但是要实现代码的简洁性和高效性还离不开一个好的软件设计模式,软件设计模式有很多种,例如,

  • 工厂模式
  • 原型模式
  • 单例模式
  • 生成器模式
  • ......

使用合理的软件设计模式可以使得代码重用性更高、更易于理解、可靠性更高。

单例模式只有一个实例存在

这是单例模式的主要特征,也是设计单例模式的要求,和普通软件设计模式允许多个实例同时存在不同,单例模式只允许一个实例存在,首先来看一个示例,

class Software(object):
    def __init__(self):
        pass

soft1 = Software()
soft2 = Software()
print(id(soft1))
print(id(soft2))

# 输出
2538846619576
2538846620024

上述给出的Python的一个普通软件设计模式,当我们定义一个名为Software的类后,我们先后实例化两个对象,分别是soft1soft2,输出它们的地址可以看出,它们不是同一个示例,这就限制了它在某些场景下无法使用,后面关于单例模式的使用场景部分会专门介绍。

单例模式有助于协调系统的整体性、统一性

由于单例模式的设计要求使得每一个应用、活动只有一个实例,这使得不管我们怎么去调用、实例化,当前唯一存在一个实例,这在资源调度、日志管理、信息注册等应用场景下保证了只有一个实例对其进行操作,而避免了多个实例同时操作一个对象,这保证了协调系统的整体性和统一性。

Python单例模式

其实,关于Python单例模式的实现,在第三讲:类的特殊方法(上篇)中已经有所提及,可以通过重写__new__方法来实现单例模式,但是Python实现单例模式不仅包含这一种方式,还可以使用装饰器来实现单例模式,下面来看一下两种实现Python单例模式的方式。

首先,定义一个名为Singleton的基类,在这个基类里面对new方法进行重写,

class Singleton(object):
    def __new__(cls, *args, **kw):
        if not hasattr(cls, '_instance'):
            orig = super(Singleton, cls)
            cls._instance = orig.__new__(cls)
        return cls._instance

然后,凡是继承Singleton基类的子类都属于单例模式,下面来看一下,

class Books(Singleton):
    def __init__(self):
        pass
 
book1 = Books()
book2 = Books()
print(id(book1))
print(id(book2))

# 输出
2538847457968
2538847457968

可以从上面输出看得出来,我们虽然对Books类实例化两次,分别得到两个名为book1和book2的实例,但是id却是相同的,也就说这两个实例指向同一个地址,为同一个实例。

装饰器

在第二讲中我详细的介绍了Python装饰器的使用,简而言之,Python装饰器就是操作函数的函数,当然,它类也可以作为装饰器的输入。利用装饰器实现Python单例模式就是通过类进行操作实现单例模式,

首先,我们完成装饰器的编写,

def singleton(cls, *args, **kw):    
    instances = {}    
    def wrapper():    
        if cls not in instances:    
            instances[cls] = cls(*args, **kw)    
        return instances[cls]    
    return wrapper  

然后调用装饰器,实现单例模式,

@singleton
class Animal(object):
    def __init__(self):
        pass
    
animal1 = Animal()
animal2 = Animal()
print(id(animal1))
print(id(animal2))

# 输出
2538848208544
2538848208544

看一下上面的输出,和new方法实现的效果是相同的。

除此之外,还可以通过__metaclass__元类、共有属性等来实现,但是由于它本质上与上述两种方式并没有什么区别,也许看代码过程中会觉得有点不太明白,其实上述两种方式都是基于同一个思想进行实现的:创建实例(instance)时首先判断是否已经存在,如果已经存在则返回,否则创建。

单例模式的使用场景

由于单例模式的特殊性,使得它具备整体性、统一性的优势,因此,它的使用场景大多数也是围绕这两点优势进行展开的,如果遇到以下场景,我们可以考虑是否能够使用单例模式来实现,

  • 资源管理的场景
  • 难以同步的场景
  • 涉及共享的场景
  • 有关认证的场景

以上述第四点展开进行讨论一下,结合代码更加容易理解单例模式的妙处所在。

场景描述

做项目开发过程中,大多数岗位都会和数据打交道,无论是前端还是后端。假如,我们存储数据工具是SQL Server,我们需要通过host、user、passwd来连接数据库进行读取数据,这时候就需要一次认证,多次调用,请注意这句话,很关键。

普通模式

我们首先来实现一个连接SQL的类,

class SqlClient(object):
    def __init__(self, host, user, passwd):
        self.host = host
        self.user = user
        self.passwd = passwd
        self.register()
    
    def register(self):
        self.info = "{}--{}---{}".format(self.host, self.user, self.passwd)
    
    def select(self):
        print("SELECT * FROM {}".format(self.host))

SqlClient中有3个方法,__init__用于初始化参数,register是认证SQL客户端,select是执行SQL语句的操作。

到这里,我们完成了SQL的认证,后面我们会在不同的地方查找数据,也就是在多个地方需要调用SqlClient类的select方法,试想一下我们该怎么实现?

有两种方法:

  • 反复实例化、反复认证
  • 把实例化后的对象作为参数传入到每个用到select的函数里

先看第一种,

host = "10.293.291.19"
user = "admin"
passwd = "666666"
def use_data_1():
    sql_client = SqlClient(host, user, passwd)
    sql_client.select()
    
def use_data_2():
    sql_client = SqlClient(host, user, passwd)
    sql_client.select()

def use_data_3():
    sql_client = SqlClient(host, user, passwd)
    sql_client.select()
    
use_data_1()
use_data_2()
use_data_3()

# 输出
SELECT * FROM 10.293.291.19
SELECT * FROM 10.293.291.19
SELECT * FROM 10.293.291.19

可以看到,我们在use_data_1、use_data_2、use_data_3三处使用到了SQL选择工具,每一次我们都要重新实例化SqlClient,显然,这是很麻烦的。

然后再看一下第二种方式,

host = "10.293.291.19"
user = "admin"
passwd = "666666"
def use_data_1(sql_client):
    sql_client.select()
    
def use_data_2(sql_client):
    sql_client.select()

def use_data_3(sql_client):
    sql_client.select()
    
sql_client = SqlClient(host, user, passwd)
use_data_1(sql_client)
use_data_2(sql_client)
use_data_3(sql_client)

我们可以先对实例化SqlClient,然后作为参数传入到每一个用到SQL工具的地方。

这样看来显然比第一种要好很多,在代码简洁性方面比第一种方法优化了不少,但是,开发中我们应该意识到一个问题,尽量少传参数,尤其是链式调用的函数,只在其中某几个环境用到,我们却需要不断的把它当作参数一致往下传递,如果这样的话,我们会发现,我们会传递很多参数,例如下面这个示例,

host = "10.293.291.19"
user = "admin"
passwd = "666666"
def use_data_1(sql_client):
    sql_client.select()
    use_data_2(sql_client)
    
def use_data_2(sql_client):
    use_data_3(sql_client)

def use_data_3(sql_client):
    sql_client.select()
    
sql_client = SqlClient(host, user, passwd)
use_data_1(sql_client)

可以看到上述示例,use_data_1调用use_data_2,use_data_2调用use_data_3,而我们在use_data_1、use_data_3中需要用到SQL工具,但是在use_data_2这个中间环节用不到,但是为了让参数继续传递下去,sql_client却不得不作为use_data_2的一个入参。

单例模式

这时候我们就可以使用单例模式来轻松解决这个问题,我们只需要实例化一次用于认证,然后再每个位置调用即可,

class Singleton(object):
    def __new__(cls, *args, **kw):
        if not hasattr(cls, '_instance'):
            orig = super(Singleton, cls)
            cls._instance = orig.__new__(cls)
        return cls._instance
    
class SqlClient(Singleton):
    info = None
    
    def register(self, host, user, passwd):
        self.info = "{}--{}--{}".format(host, user, passwd)
        
    def select(self):
        print(self.info)

我们通过继承Singleton实现SqlClient的单例模式,我们只需要调用register一次,用于认证客户端,然后后期每次重新实例化都是指向的同一个实例,也就是已经认证过的示例,我们后面任何其他地方调用的地方直接使用select方法即可,

def use_data_1():
    SqlClient().select()

def use_data_2():
    SqlClient().select()
    
def use_data_3():
    SqlClient().select()
    
SqlClient().register(host, user, passwd)
use_data_1()
use_data_2()
use_data_3()

依此可以发散思维一下,凡是类似的场景都可以考虑一下是否可以使用单例模式。

当然,凡事既有优点就会有缺点,单例模式也是,它可以实现系统的整体性和统一性,但是也不是在任何场景下都是适用的,例如,

  • 多线程
  • 可变对象

在这些场景下,它违背了单例模式单一性原则,而且很容易因此数据错误。

因此,使用单例模式之前需要考虑一下对应场景是否适合,如果适合,单例模式能够大大提高代码的效率,同时使得代码更加简洁,但是如果不适合而强行使用单例模式,那样会导致很多未知的问题。

完整代码

我把完整代码已经放在github,感兴趣的可以点击下方链接,或者直接搜索项目advance-python

https://github.com/Jackpopc/advance-python/blob/master/4-Singleton.ipynb

你可能感兴趣的:(【进阶Python】第六讲:单例模式的妙用)