Python丨为什么你学不好设计模式?
课程简介
“读万卷书不如走万里路”,设计模式学不好的根本,还是在于缺少实战。
新课《Python 设计模式基础实战》向大家介绍了编程语言的设计模式,着重强调大家动手实际操作。
总苦于无法将设计模式活学活用的你,不妨来学一下。
设计模式简介
所谓“设计模式”就是一套由前人总结的代码的设计思路。以其中最常用的“单例模式”为例,在程序中我们有一个需求,这个需求的实现有多种思路,其中一种是创建一个类并且使得该类在多次实例化时生成唯一的一个实例。这就需要设计代码实现这个结果。大家发现这种场景下这样设计是最合理的,我们就管这种设计思路叫做“单例模式”。
设计模式不分语言,大多数编程语言都可以实现。
一个设计模式并不像一个类或一个库那样能够直接作用于我们的代码。拿建造桥梁粗略地类比一下,一座桥是一个小功能,代码就是砖石瓦块钢筋水泥,设计模式就是我们要怎么建造,圆拱桥、独木桥、管道桥还是拉索桥。
设计模式很有用,但它要用到合适的场景中才能发挥应有的效果,否则可能出现弊大于利的情况。
通常来讲设计模式分为三类:
- 创建模式,提供实例化的方法,为适合的状况提供相应的对象创建方法。
- 结构模式,通常用来处理实体之间的关系,使得这些实体能够更好地协同工作。
- 行为模式,用于在不同的实体建进行通信,为实体之间的通信提供更容易、更灵活的通信方法。
单例模式
所谓单例模式,也就是说任何时候我们都要确保只有一个对象实例存在。很多情况下,整个系统中只需要存在一个对象,所有的信息都从这个对象获取,比如系统的配置对象,或者是线程池。这些场景下,就非常适合使用单例模式。
总结起来,就是说不管我们实例化一个类多少次,真正干活的对象只会生成一次并且在首次实例化时生成。
在 Python 中实现单例模式的方式有很多,下面分别举例说明。
1)使用嵌套类实现
在定义类 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:', </pre>
如上所示,对代码进行简略说明:
# 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 的实例上。
2)使用装饰器实现
以上代码虽然很好地实现了单例模式,但是在真正的项目开发中这种方式却不够灵活,因为我们要将真正干活的类内置在单例类中,这会有些麻烦,例如删除实例的属性这一点就不太好实现。
下面我们使用 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
来存储实例的。通过使用装饰器的模式我们将代码解耦了,使用更加灵活。
其实这里我们也用到装饰者模式啦,后面的章节会介绍。
3)重写 new 方法
在对类进行实例化时,需要先调用类的 __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
总结
本节实验内容来自于《Python 设计模式基础实战》,主要介绍了设计模式的基本概念,并对创建型模式之一单例模式的实现进行了讲解,其中涉及到三个方法,它们可以根据实际场景进行恰当地选用。所有的设计都是为了实现对某个类的实例进行唯一的限制。
后续实验我们将讲解其他模式,以及一些额外的关于“元类”的知识等。
你将学到:
点击《Python 设计模式基础实战》,学习完整课程。