前言
在第三讲:类的特殊方法(上篇)中我在讲解Python特殊方法__new__的使用时提及了一个概念--单例模式,这是一个软件设计中非常重要的概念,由于它不属于某一类特定的语言,既可以用于Java、也可以用于Python,因此在这些单一编程语言的书籍里很少特意花费篇幅介绍单例模式,因此,我准备用这整篇文章来介绍一下Python的单例模式的实现及使用场景。
本文,我将从如下3个方面阐述Python单例模式的使用,
- 单例模式的概念
- Python单例模式的实现
- 单例模式的使用场景
单例模式
首先看一下维基百科对单例模式的解释,
单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
上述描述也许有点让人云里雾里,我来提炼一下维基百科关于单例模式解释的关键点,
- 单例模式是一种软件设计模式,而不是专属于某种编程语言的语法;
- 单例模式只有一个实例存在;
- 单例模式有助于协调系统的整体性、统一性;
软件设计模式
我一直认为,对于一门编程语言“入门容易,精通不易”,哪怕是对于很多人都认为简单的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的类后,我们先后实例化两个对象,分别是soft1和soft2,输出它们的地址可以看出,它们不是同一个示例,这就限制了它在某些场景下无法使用,后面关于单例模式的使用场景部分会专门介绍。
单例模式有助于协调系统的整体性、统一性
由于单例模式的设计要求使得每一个应用、活动只有一个实例,这使得不管我们怎么去调用、实例化,当前唯一存在一个实例,这在资源调度、日志管理、信息注册等应用场景下保证了只有一个实例对其进行操作,而避免了多个实例同时操作一个对象,这保证了协调系统的整体性和统一性。
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()
依此可以发散思维一下,凡是类似的场景都可以考虑一下是否可以使用单例模式。
当然,凡事既有优点就会有缺点,单例模式也是,它可以实现系统的整体性和统一性,但是也不是在任何场景下都是适用的,例如,
- 多线程
- 可变对象
在这些场景下,它违背了单例模式单一性原则,而且很容易因此数据错误。
因此,使用单例模式之前需要考虑一下对应场景是否适合,如果适合,单例模式能够大大提高代码的效率,同时使得代码更加简洁,但是如果不适合而强行使用单例模式,那样会导致很多未知的问题。