世界上只有两种程序员:「懂设计模式的」和「不懂设计模式的」。
懂设计模式的程序员,写出来的代码优雅如诗,易读易维护,扩展性也更强;不懂设计模式的程序员,代码随心所欲,但写完回头一看,往往自己都会一头雾水,可维护性几乎为零。
设计模式(Design Pattern)是几十年的开发人员经过长时间的试验和摸索,总结出来的一套 程序设计标准。它使得设计方案更加通俗易懂 —— 无论你使用哪种编程语言,做什么类型的项目,甚至处在一个国际化的开发团队,当面对同一个设计模式时,你和别人的理解就别无二致。
几乎所有的大型软件、编程语言、框架都使用了这些标准,也因此,设计模式变成了面试必考的问题,也是程序员的基本功之一。
学习和使用设计模式,可以:
1. 提高代码的可读性、可靠性、可复用性,使编程真正工程化;
2. 便于后期维护项目,增强系统的健壮性和扩展性;
3. 可以锻炼程序员的设计思维,提高代码质量。
下面,我们就来用 Python 学习 12 种最经典的设计模式,从单例模式讲起:
单例模式
设计模式是一套被反复使用且经过验证的比较高效的代码设计经验,对于我们经常会遇到的一些编程问题而言,它们是比较可靠的解决方案。
设计模式不分语言,大多数编程语言都可以实现。我们的课程针对 Python 实现最常见的几种设计模式进行讲解。
设计模式简介
单例模式的三种实现方式
所谓“设计模式”就是一套由前人总结的代码的设计思路。以其中最常用的“单例模式”为例,在程序中我们有一个需求,这个需求的实现有多种思路,其中一种是创建一个类并且使得该类在多次实例化时生成唯一的一个实例。这就需要设计代码实现这个结果。大家发现这种场景下这样设计是最合理的,我们就管这种设计思路叫做“单例模式”。
设计模式不分语言,大多数编程语言都可以实现。
一个设计模式并不像一个类或一个库那样能够直接作用于我们的代码。拿建造桥梁粗略地类比一下,一座桥是一个小功能,代码就是砖石瓦块钢筋水泥,设计模式就是我们要怎么建造,圆拱桥、独木桥、管道桥还是拉索桥。
设计模式很有用,但它要用到合适的场景中才能发挥应有的效果,否则可能出现弊大于利的情况。
通常来讲设计模式分为三类:
创建模式,提供实例化的方法,为适合的状况提供相应的对象创建方法。
结构模式,通常用来处理实体之间的关系,使得这些实体能够更好地协同工作。
行为模式,用于在不同的实体建进行通信,为实体之间的通信提供更容易、更灵活的通信方法。
所谓单例模式,也就是说任何时候我们都要确保只有一个对象实例存在。很多情况下,整个系统中只需要存在一个对象,所有的信息都从这个对象获取,比如系统的配置对象,或者是线程池。这些场景下,就非常适合使用单例模式。总结起来,就是说不管我们实例化一个类多少次,真正干活的对象只会生成一次并且在首次实例化时生成。
在 Python 中实现单例模式的方式有很多,下面分别举例说明。
在定义类 A 时,在类中再定义一个嵌套类 _A。首次对类 A 进行实例化时,将类 _A 的实例赋值给类 A 的属性 _instance ,然后给类 A 的实例定义一个 __getattr__
方法,使得类 A 的实例调用自身属性或方法时,都去调用类的 _instance 属性,也就是类 _A 的实例的属性和方法。类属性是固定不变的,所以类 A 的实例虽然是不同的,但它们的属性和方法都是完全一样的。
将如下代码写入 singleton_1.py
文件中:
class Singleton:
'''
单例模式
'''
# 创建一个嵌套类
class _A:
def display(self): # 1
return id(self)
_instance = None
def __init__(self): # 2
__class__._instance = __class__._instance or __class__._A()
def __getattr__(self, attr): # 3
return getattr(__class__._instance, attr)
def __setattr__(self, attr, value): # 4
object.__setattr__(__class__._instance, attr, value)
if __name__ == '__main__':
s1 = Singleton(); s2 = Singleton() # 5
print('id(s1):', id(s1)) # 6
print('id(s2):', id(s2))
print('s1.display():', s1.display()) # 7
print('s2.display():', s2.display())
s1.name = 'James' # 8
print('s1.name:', s1.name)
print('s2.name:', s2.name)
如上所示,对代码进行简略说明:
# 1
创建一个嵌套类 _A,在类内部定义一个 display 方法,该方法返回 _A 类的实例的内存地址。
# 2
编写 Singleton 类的实例的初始化方法。创建 Singleton 类的实例后,执行此方法。方法内部是对类的操作,为类的 _instance 属性赋值一个 _A 的实例。第一次对 Singleton 进行实例化时会创建一个 _A 类的实例并赋值,以后不再变化。
# 3
编写 Singleton 类的实例获取属性的方法。Singleton 类内部故意不为自身的实例设置任何属性,结果就是调用实例的属性时最后落到此方法的头上。方法内部获取类属性 _instance 的同名属性,也就是 _A 类的实例的属性。
# 4
编写 Singleton 类的实例定义属性的方法。同样,此方法内部调用object.__setattr__
方法为 Singleton._instance 也就是 _A 的实例定义属性。
# 5
为 Singleton 类创建两个实例以备测试。
# 6
打印两个实例的内存地址,它们的结果应该是不同的。
# 7
打印两个实例调用 display 方法的结果,实际上调用的都是Singleton._instance
的同名方法,结果应该是一样的。
# 8
其中一个实例定义 name 属性,然后两个实例获取该属性并打印,结果应该都是一样的。
终端执行脚本,操作结果如下所示:
$ python3 singleton_1.py
id(s1): 4330824208
id(s2): 4330824336
s1.display(): 4330824272
s2.display(): 4330824272
s1.name: James
s2.name: James
如上所示,Singleton 的实例各不相同,它们在赋值属性和调用属性时,结果却是相同的。因为这些实例操作属性时都转移到了嵌套类 _A 的实例上。
以上代码虽然很好地实现了单例模式,但是在真正的项目开发中这种方式却不够灵活,因为我们要将真正干活的类内置在单例类中,这会有些麻烦,例如删除实例的属性这一点就不太好实现。
下面我们使用 Python 装饰器来实现单例模式。
首先创建一个「类装饰器」,也就是编写一个类,这个类作为装饰器。这个「类装饰器」在实例化的时候,将另一个类作为参数。类装饰器的名字是 SingletonDeco ,其 __call__
方法就是实例调用自身所执行的方法,我们可以把此方法的返回值定义为唯一的对象。
将如下代码写入 singleton_2.py
文件中:
class SingletonDeco:
"""
单例类装饰器
"""
def __init__(self, cls): # 1
print('装饰器初始化')
self._cls = cls
def instance(self): # 2
try:
return self._instance
except AttributeError:
self._instance = self._cls()
return self._instance
def __call__(self): # 3
return self.instance()
@SingletonDeco # 4
class Singleton:
def display(self):
return id(self)
if __name__ == '__main__':
s1 = Singleton() # 5
s2 = Singleton()
print('id(s1):', s1.display())
print('id(s2):', s2.display())
print('s1 is s2:', s1 is s2)
对代码进行简单描述:
# 1
类装饰器的初始化方法,将被装饰的类赋值给实例的 _cls 属性。
# 2
此方法用于给实例的 _instance 属性赋值,此方法的调用权交个了__call__
方法,也就是说调用 SingletonDeco 类的实例时会执行 instance 方法并返回被装饰器装饰的类 Singleton 的实例。并且不论调用多少次,结果都是一样的。
# 3
类装饰器 SingletonDeco 的实例的调用接口。
# 4
使用类装饰器创建 Singleton 类,创建该类时,会执行SingletonDeco.__init__
方法,并且将该类赋值给实例的 _cls 属性。此时 Singleton 这个变量就指向了 SingletonDeco 这个类的实例。如果要获取原 Singleton 类,就需要调用 Singleton 的 _cls 属性。此外原 Singleton 类为实例提供了 display 方法返回实例的内存地址。
# 5
调用 Singleton ,表面上看是对 Singleton 类进行实例化,实际上是调用 SingletonDeco 类的实例的 __call__
方法。因为变量 Singleton 指向的就是 SingletonDeco 的实例。调用 __call__
的结果就是调用 instance
方法,下一步就是对调用实例的 _cls
属性,而这个属性的值就是原 Singleton 类。综上所述,这个最终的调用结果还是原 Singleton 的实例。绕这么大一圈,一切都是为了在 instance 方法中实现唯一实例。
终端执行结果如下:
$ python3 singleton_2.py
装饰器初始化
id(s1): 4350000976
id(s2): 4350000976
s1 is s2: True
以上代码中,我们用装饰器实现了单例模式,任何想使用单例模式的类,只需要使用Singleton
装饰器装饰一下就可以使用了。可以看到其核心工作原理其实和第一种实现方式是一致的,也是使用内置的属性 Singleton._instance
来存储实例的。通过使用装饰器的模式我们将代码解耦了,使用更加灵活。其实这里我们也用到装饰者模式啦,后面的章节会介绍。
在对类进行实例化时,需要先调用类的 __new__
方法创建实例,再调用实例的__init__
方法初始化。所以要实现单例模式,可以在类的 __new__
方法中做文章。
首先判断类属性 __instance
是否存在。注意,使用 hasattr 方法时,相当于在类的外部调用类属性,私有属性的命令是一个下划线加类名加属性名。如果该属性不存在,调用根父类 object 的 __new__
生成一个 Singleton 类的实例并赋值给类属性 __instance
。接下来打印类属性 __instance
的内存地址,实际上就是类的实例的内存地址。最后返回该实例。
在对类进行实例化时,只有首次会创建类的实例,之后都是返回类的 __instance
属性值。这样设计就可以实现单例模式了。
将如下代码写入 singleton_3.py
文件:
class Singleton:
def __new__(cls, *args, **kw):
if not hasattr(cls, '_Singleton__instance'):
cls.__instance = super().__new__(cls, *args, **kw)
print('实例化时打印实例 ID:', id(cls.__instance))
return cls.__instance
s1 = Singleton()
s2 = Singleton()
print('s1 is s2:', s1 is s2)
其中 super().__new__
等同于 object.__new__
。
如果不重写类的 __new__
方法,则默认调用 object 的同名方法并返回,也就是每次都会创建一个新的实例。重写的目的就是将一个实例固定到类属性中,然后每次创建实例时都返回这个属性值。这个方式思路简单,代码也很清晰。
终端执行程序结果如下:
$ python3 singleton_3.py
实例化时打印实例 ID: 4480060240
实例化时打印实例 ID: 4480060240
s1 is s2: True
本节实验主要介绍了设计模式的基本概念,并对创建型模式之一单例模式的实现进行了讲解,其中涉及到三个方法,它们可以根据实际场景进行恰当地选用。所有的设计都是为了实现对某个类的实例进行唯一的限制。
下一节实验我们将讲解一些额外的关于“元类”的知识,实际上它们用得很少,但这有助于我们理解一些特别的代码。
❝ 篇幅有限,点击右下角「阅读原文」,学习 12 种 Python 设计模式!
❞
完