【进阶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()

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

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

  • 多线程
  • 可变对象

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

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

相关文章
|
19天前
|
设计模式 存储 数据库连接
Python编程中的设计模式之美:单例模式的妙用与实现###
本文将深入浅出地探讨Python编程中的一种重要设计模式——单例模式。通过生动的比喻、清晰的逻辑和实用的代码示例,让读者轻松理解单例模式的核心概念、应用场景及如何在Python中高效实现。无论是初学者还是有经验的开发者,都能从中获得启发,提升对设计模式的理解和应用能力。 ###
|
2月前
|
设计模式 缓存 数据库
Python中的单例模式
单例模式确保一个类只有一个实例,并提供全局访问点。此模式常用于共享资源或限制资源访问,可避免资源浪费并简化全局访问。其实现方法包括使用模块级变量、装饰器或元类。尽管单例模式能简化访问和初始化,但也可能引入全局状态,影响代码的可维护性。适用于配置管理、数据库连接池等场景。
|
4月前
|
数据采集 网络协议 数据挖掘
网络爬虫进阶之路:深入理解HTTP协议,用Python urllib解锁新技能
【7月更文挑战第30天】网络爬虫是数据分析和信息聚合的关键工具。深入理解HTTP协议及掌握Python的urllib库对于高效爬虫开发至关重要。HTTP协议采用请求/响应模型,具有无状态性、支持多种请求方法和内容协商等特点。
48 3
|
4月前
|
网络协议 开发者 Python
网络编程小白秒变大咖!Python Socket基础与进阶教程,轻松上手无压力!
【7月更文挑战第25天】在网络技术快速发展的背景下, Python因其简洁的语法和强大的库支持成为学习网络编程的理想选择。
67 5
|
4月前
|
机器学习/深度学习 数据采集 算法
Python编程语言进阶学习:深入探索与高级应用
【7月更文挑战第23天】Python的进阶学习是一个不断探索和实践的过程。通过深入学习高级数据结构、面向对象编程、并发编程、性能优化以及在实际项目中的应用,你将能够更加熟练地运用Python解决复杂问题,并在编程道路上走得更远。记住,理论知识只是基础,真正的成长来自于不断的实践和反思。
|
4月前
|
网络协议 Python
网络世界的建筑师:Python Socket编程基础与进阶,构建你的网络帝国!
【7月更文挑战第26天】在网络的数字宇宙中,Python Socket编程是开启网络世界大门的钥匙。本指南将引领你从基础到实战,成为网络世界的建筑师。
63 2
|
4月前
|
开发者 Python
Python Socket编程:不只是基础,更有进阶秘籍,让你的网络应用飞起来!
【7月更文挑战第25天】在网络应用蓬勃发展的数字时代,Python凭借其简洁的语法和强大的库支持成为开发高效应用的首选。本文通过实时聊天室案例,介绍了Python Socket编程的基础与进阶技巧,包括服务器与客户端的建立、数据交换等基础篇内容,以及使用多线程和异步IO提升性能的进阶篇。基础示例展示了服务器端监听连接请求、接收转发消息,客户端连接服务器并收发消息的过程。进阶部分讨论了如何利用Python的`threading`模块和`asyncio`库来处理多客户端连接,提高应用的并发处理能力和响应速度。掌握这些技能,能使开发者在网络编程领域更加游刃有余,构建出高性能的应用程序。
32 3
|
4月前
|
SQL 安全 Go
SQL注入不可怕,XSS也不难防!Python Web安全进阶教程,让你安心做开发!
【7月更文挑战第26天】在 Web 开发中, SQL 注入与 XSS 攻击常令人担忧, 但掌握正确防御策略可化解风险. 对抗 SQL 注入的核心是避免直接拼接用户输入至 SQL 语句. 使用 Python 的参数化查询 (如 sqlite3 库) 和 ORM 框架 (如 Django, SQLAlchemy) 可有效防范. 防范 XSS 攻击需严格过滤及转义用户输入. 利用 Django 模板引擎自动转义功能, 或手动转义及设置内容安全策略 (CSP) 来增强防护. 掌握这些技巧, 让你在 Python Web 开发中更加安心. 安全是个持续学习的过程, 不断提升才能有效保护应用.
53 1
|
3月前
|
设计模式 存储 数据库连接
Python设计模式:巧用元类创建单例模式!
Python设计模式:巧用元类创建单例模式!
47 0
|
4月前
|
存储 算法 搜索推荐
算法进阶之路:Python 归并排序深度剖析,让数据排序变得艺术起来!
【7月更文挑战第12天】归并排序是高效稳定的排序算法,采用分治策略。Python 实现包括递归地分割数组及合并已排序部分。示例代码展示了如何将 `[12, 11, 13, 5, 6]` 分割并归并成有序数组 `[5, 6, 11, 12, 13]`。虽然 $O(n log n)$ 时间复杂度优秀,但需额外空间,适合大规模数据排序。对于小规模数据,可考虑其他算法。**
76 4