简介
在本指南中,我们将看看_Python中原型设计模式_的理论和实现,以及何时可以从利用它中受益。
面向对象的编程(OOP)范式
_设计模式是对常见问题的解决方案,通常存在,但不限于 面向对象的编程(OOP)架构。OOP是最常见的编程范式之一,由于它的直观性和它能很好地反映现实世界。通过OOP,我们将物理世界抽象为软件,使我们能够自然地观察和编写代码。每个实体都成为一个_对象,这些对象可以与其他对象发生关系--在一个系统中形成一个对象的层次结构。
虽然这种方法对我们来说是非常直观和自然的,但事情很快就会变得紧张起来,就像现实世界一样。由于有许多关系、互动和结果--很难以一种连贯的方式维持所有的东西。无论是创造、结构还是行为,扩展这些系统都会变得非常棘手,而且每走错一步,你就会在问题上陷得更深。_这_就是为什么设计模式在今天被应用和广泛使用的原因。
ABC库
OOP范式通常利用_抽象类_的使用,这在Python中不是一个内置的功能。为了实现这一功能,我们使用了 ABC(Abstract Base Classes)库.
通过ABC,我们将能够定义抽象类并在此基础上形成子类,这使我们能够实现这一模式。
设计模式
同样,设计模式是标准化的实践和结构,帮助我们在OOP架构中建立可扩展的、干净的实现。它们通常提供了一个在编写代码时要遵循的基础结构,只要你遵循模式的基本概念,就可以进行定制。
有三个主要的设计模式类别。
- 创造性设计模式--关注于实现对象的创建,同时抽象/隐藏了对象的创建逻辑。
- 结构性设计模式--旨在处理对象和类的组成,依靠继承来控制对象的和结构。
- 行为设计模式--专注于对象之间发生的通信,控制数据在对象之间的移动方式,以及在类之间分配行为。
原型模式的直观性
原型模式是一种用于克隆原型对象的创造型设计模式,它是一个定义基本属性的超类。自然地,子类也有同样的基本属性,并有一些自己的特殊属性。
当克隆是一个比创建一个新对象更便宜的操作时,以及当创建需要长时间、昂贵的调用时,原型设计模式通常被应用。这些调用通常与昂贵的数据库操作相联系,但也可能是其他昂贵的过程。
为了模拟这一点--我们将在创建对象的过程中模拟一个昂贵的过程调用,持续整整三秒。然后,使用原型设计模式--我们将创建新的对象,同时避开这个限制。
为了促进这一功能,我们将使用两个类。
- Prototype。超类,它将包含所有的基本义务属性和方法,当克隆复制到
Prototype。另外,原型类有一个抽象的clone()方法,所有的子类都必须实现这个方法。 - 具体的类(es)。一旦我们创建了
Prototype,我们就可以开始定义基于它的具体类。具体类可以有自己的属性和方法,但它们总是有原始的原型属性和重写的clone()。
原型模式在 Python 中的实现
我们将为一个虚构的视频游戏创建几个NPC类型--一个Shopkeeper ,一个Warrior 和一个Mage 。
他们每个人都是NPC,一个共同的超类--但他们会有不同的属性。Shopkeeper 有charisma ,所以他们可以更好地进行交易,而Mage 有mana ,而不是stamina ,像Warrior 那样。
我们的Prototype 类将标志着一般的NPC,从它开始,我们可以实现我们的具体类。我们将在Prototype 构造函数和具体的类本身_都_有延迟,在构造函数中模拟一个昂贵的调用--将代码执行延迟几秒,使任何新对象的创建成为一个极其昂贵的操作。
最后,由于这些类将无法以合理的方式使用--我们将利用Prototype模式来缓解这一问题并恢复性能。
定义原型类
让我们从超类开始--NPC的Prototype 。它的clone() 方法将是空的,但它的子类将实现它。当然,它也将包含所有子类的基本属性。因为我们希望所有的子类都必须实现clone() 方法,所以它被标记为一个@abstractmethod 。这个注解源于ABC库,抽象方法不提供实现,但必须由子类来实现。
from abc import ABC, abstractmethod
import time
# Class Creation
class Prototype(ABC):
# Constructor:
def __init__(self):
# Mocking an expensive call
time.sleep(3)
# Base attributes
self.height = None
self.age = None
self.defense = None
self.attack = None
# Clone Method:
@abstractmethod
def clone(self):
pass
具体类
现在,让我们基于Prototype 来定义我们的具体类。我们将重写clone() 方法,实际上为它提供一个实现。为了复制对象,我们将使用copy 库,它是内置在 Python 中的。该库的copy() 方法执行一个 _浅层拷贝_对象,而deepcopy() 则创建一个 _深拷贝_的对象。取决于你的对象的结构--你会更喜欢其中一个。
_浅层拷贝_将只是复制对非原始字段的引用,如字典、列表、集合或其他类。_深度拷贝_将创建新的实例,具有相同的数据。这意味着对于浅层拷贝--你最终可能会改变_多个类的_字段,而不是一个,因为多个对象可能通过同一个引用共享同一个字段。
浅层拷贝是比较便宜的操作,因为它们不会为非原始类型实例化任何新东西。一般来说,这些类型的实例化可能并不昂贵,所以你不会有什么收获。但是,如果你的类也有昂贵的字段--那些需要时间来实例化的字段,浅层拷贝将比深层拷贝更有性能,代价是在内存中共享相同的对象。
既然如此,让我们来定义我们的子类。我们将为这些具体类的所有实例提供一些基值,而不是通过构造函数接受值。
from prototype import Prototype
import copy
import time
class Shopkeeper(Prototype):
def __init__(self, height, age, defense, attack):
super().__init__()
# Mock expensive call
time.sleep(3)
self.height = height
self.age = age
self.defense = defennse
self.attack = attack
# Subclass-specific Attribute
self.charisma = 30
# Overwritting Cloning Method:
def clone(self):
return copy.deepcopy(self)
Warrior NPC有另一组基值。
from prototype import Prototype
import copy
import time
class Warrior(Prototype):
def __init__(self, height, age, defense, attack):
# Call superclass constructor, time.sleep() and assign base values
# Concrete class attribute
self.stamina = 60
# Overwritting Cloning Method
def clone(self):
return copy.deepcopy(self)
最后,Mage 。
from prototype import Prototype
import copy
import time
class Mage(Prototype):
def __init__(self, height, age, defense, attack):
# Call superclass constructor, time.sleep() and assign base values
self.mana = 100
# Overwritting Cloning Method
def clone(self):
return copy.deepcopy(self)
测试Python中的原型设计模式
现在,我们可以测试这个模式了。首先,我们将创建一个Shopkeeper 的实例,记下它所花费的时间。
print('Starting to create a Shopkeeper NPC: ', datetime.datetime.now().time())
shopkeeper = Shopkeeper(180, 22, 5, 8)
print('Finished creating a Shopkeeper NPC: ', datetime.datetime.now().time())
print('Attributes: ' + ', '.join("%s: %s" % item for item in vars(shopkeeper).items()))
这导致了6秒的等待--3秒来自Prototype ,3秒来自Shopkeeper ,但最终在6秒后创建了该对象。
Starting to create a Shopkeeper NPC: 15:57:40.852336
Finished creating a Shopkeeper NPC: 15:57:46.859203
Attributes: height: 180, age: 22, defense: 5, attack: 8, charisma: 30
正如预期的那样,这是一个令人痛苦的缓慢操作。如果我们需要另一个店主会怎样?或者更好的是--如果我们还需要5个店主呢?让我们实例化一个包含5个店主的行会。
print('Instantiating trader guild at: ', datetime.datetime.now().time())
for i in range(5):
shopkeeper = Shopkeeper(180, 22, 5, 8)
print(f'Finished creating a Shopkeeper NPC {i} at: ', datetime.datetime.now().time())
print('Finished instantiating trader guild at: ', datetime.datetime.now().time())
Instantiating trader guild at: 16:15:14.353285
Finished creating a Shopkeeper NPC 0 at: 16:15:20.360971
Finished creating a Shopkeeper NPC 1 at: 16:15:26.365997
Finished creating a Shopkeeper NPC 2 at: 16:15:32.370327
Finished creating a Shopkeeper NPC 3 at: 16:15:38.378361
Finished creating a Shopkeeper NPC 4 at: 16:15:44.383375
Finished instantiating trader guild at: 16:15:44.383674
相反,我们可以_克隆_ 第一个店主,考虑到他们都遵循相同的模式,并且可以被替换。
print('Instantiating trader guild at: ', datetime.datetime.now().time())
shopkeeper_template = Shopkeeper(180, 22, 5, 8)
for i in range(5):
shopkeeper_clone = shopkeeper_template.clone()
print(f'Finished creating a Shopkeeper clone {i} at: ', datetime.datetime.now().time())
print('Finished instantiating trader guild at: ', datetime.datetime.now().time())
这就导致了。
Instantiating trader guild at: 16:19:24.965780
Finished creating a Shopkeeper clone 0 at: 16:19:30.975445
Finished creating a Shopkeeper clone 1 at: 16:19:30.975763
Finished creating a Shopkeeper clone 2 at: 16:19:30.975911
Finished creating a Shopkeeper clone 3 at: 16:19:30.976058
Finished creating a Shopkeeper clone 4 at: 16:19:30.976132
Finished instantiating trader guild at: 16:19:30.976529
现在,只需要将第一个模板Shopkeeper ,我们就可以在_纳秒_内克隆它。整个5个克隆只需要_0.001_秒就可以完成。
现在,我们可以毫无问题地创建整个不同的NPC群体。
print('Instantiating 1000 NPCs: ', datetime.datetime.now().time())
shopkeeper_template = Shopkeeper(180, 22, 5, 8)
warrior_template = Warrior(185, 22, 4, 21)
mage_template = Mage(172, 65, 8, 15)
for i in range(333):
shopkeeper_clone = shopkeeper_template.clone()
warrior_clone = warrior_template.clone()
mage_clone = mage_template.clone()
print(f'Finished creating NPC trio clone {i} at: ', datetime.datetime.now().time())
print('Finished instantiating NPC population at: ', datetime.datetime.now().time())
这导致了1000个拷贝,所有这些拷贝总共花了0.1秒。
Instantiating 1000 NPCs: 16:27:14.566635
Finished creating NPC trio clone 0 at: 16:27:32.591992
...
Finished creating NPC trrio clone 331 at: 16:27:32.625681
Finished creating NPC trio clone 332 at: 16:27:32.625764
Finished instantiating NPC population at: 16:27:32.625794
结论
创建复杂的对象,特别是当它们需要昂贵的数据库调用时是很耗时的。
在本指南中,我们看了如何在Python中实现原型设计模式,并展示了在使用它来克隆昂贵的实例而不是创建新实例时性能的巨大提升。