ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] 在上一章中,我们简要介绍了设计模式,并介绍了迭代器模式,一种非常有用和常见的模式,它已经被抽象到编程语言本身的核心。在本章中,我们将回顾其他一些常见模式,以及它们在Python中的实现方式。同迭代一样,Python经常提供替代语法来更简单解决问题。我们将涵盖“传统”设计和这些模式的Python版本。总之,我们将看到: * 众多特定模式 * Python中每个模式的规范实现 * 取代某些模式的Python语法 ## 装饰器模式 装饰模式允许我们用其他对象“包装”一个提供核心功能的对象,这些对象可以改变核心功能。任何使用装饰对象的对象与装饰对象交互的方式,就像没有被装饰一样(就是说,被装饰对象的接口仍然与核心对象的接口是一样的)。 </b> 装饰模式有两个主要用途: * 增强组件向第二个组件发送数据时的响应内容 * 支持多种可选行为 第二种选择通常是多重继承合适的替代方案。我们可以构建一个核心对象,然后在核心周围创建一个装饰器。由于装饰对象与核心对象具有相同的接口,我们甚至可以使用其他装饰对象包装这个新对象。下面是它在 UML 中的样子: ![](https://box.kancloud.cn/64a7b7f85d4a945e953b993c98279246_461x174.png) </b> 在这里,**核心**和所有装饰器均实现一个特定的**接口**。装饰器通过组合维护对接口另一个实例的引用。当被调用的时候,装饰器会在调用其包装的接口之前或之后进行一些额外的处理。包装对象可以是另一个装饰器,或者核心功能。 虽然多个装饰者可以相互包装,但处在所有装饰器“中心”的对象提供了核心功能。 ### 装饰器例子 让我们看一个网络编程的例子。我们将使用一个TCP套接字。`socket.send()`方法获取一串输入字节,并将它们输出到另一端的套接字。有很多库可以接受套接字,并通过访问此函数发送流数据。让我们创建这样一个对象;它是一个交互式shell,等待来自客户端的连接,然后提示用户输入字符串: ``` import socket def respond(client): response = input("Enter a value: ") client.send(bytes(response, 'utf8')) client.close() server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(('localhost',2401)) server.listen(1) try: while True: client, addr = server.accept() respond(client) finally: server.close() ``` `respond`函数接受套接字参数,并提示发送数据作为回复,然后发送。为了使用它,我们构建了一个服务器套接字,并告诉它监听本地计算机的端口2401(我随机选择了这个端口)。当客户连接时,它调用`respond`函数,该函数交互请求数据并恰当的回复。需要注意的重要一点是`respond`函数只关心套接字接口的两种方法:发送`send`和关闭`clolse`。为了测试这个,我们可以编写一个非常简单的客户端,它连接到同一个端口并在退出前输出响应: ``` import socket client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(('localhost', 2401)) print("Received: {0}".format(client.recv(1024))) client.close() ``` 使用这些程序: 1. 在一个终端中启动服务器。 2. 打开第二个终端窗口并运行客户端。 3. 在服务器窗口的“输入值:”提示中,键入一个值,然后按回车。 4. 客户端将接收你键入的内容,将其打印到控制台,然后退出。再次运行客户端;服务器提示输入第二个值。 现在,再看看我们的服务器代码,我们看到两个部分。`respond`函数将数据发送到套接字对象中。剩下的脚本负责创建套接字对象。我们将创建一对定制套接字行为的装饰器,而不必扩展或修改套接字本身。 </b> 让我们从一个“日志”装饰器开始。该对象将数据发送到客户端之前将数据输出到服务器控制台: ``` class LogSocket: def __init__(self, socket): self.socket = socket def send(self, data): print("Sending {0} to {1}".format( data, self.socket.getpeername()[0])) self.socket.send(data) def close(self): self.socket.close() ``` 这个类修饰一个套接字对象,并向客户端套接字展示`send`和`close`接口。一个好的装饰器也会实现(并且可能是定制)剩下所有的套接字方法。它应该正确地实现所有待发送的参数(它实际上也接受可选的标记参数),但是让我们的例子尽可能保持简单!每当对此对象调用`send`方法时,它都会在使用原始套接字向客户端发送数据之前将记录输出到屏幕上。我们只需要修改原始代码中的一行就可以使用这个装饰器。我们不用套接字调用`respond`,我们用修饰后的套接字调用它: ``` respond(LogSocket(client)) ``` 虽然这很简单,但我们必须问问自己,为什么我们不扩展套接字类并重写`send`方法。我们可以在写完日志后,调用`super().send`去发送实际的内容。这种设计也没有错。当面临在装饰器和继承之间进行选择时,如果我们需要根据某些条件动态修改对象,我们应该只使用装饰器。例如,如果服务器当前处于调试模式,我们想做的只是让日志装饰器生效。当我们有不止一种可选行为,装饰器也比多重继承好。举个例子,我们可以写另外一个装饰器,每当我们调用`send`时,使用`gzip`来压缩数据: ``` import gzip from io import BytesIO class GzipSocket: def __init__(self, socket): self.socket = socket def send(self, data): buf = BytesIO() zipfile = gzip.GzipFile(fileobj=buf, mode="w") zipfile.write(data) zipfile.close() self.socket.send(buf.getvalue()) def close(self): self.socket.close() ``` 此版本中的`send`方法在把传入的数据压缩后再发送给客户。 </b> 现在我们有了这两个装饰器,我们可以动态地编写代码在它们之间切换不同的响应。这个例子并不完整,但它说明了我们可能遵循的混合和匹配装饰者的逻辑: ``` client, addr = server.accept() if log_send: client = LoggingSocket(client) if client.getpeername()[0] in compress_hosts: client = GzipSocket(client) respond(client) ``` 这段代码检查一个名为`log_send`的假设配置变量。如果这个变量启用了,它将套接字包装在`LoggingSocket`装饰器中。同样,它检查 已连接的客户端是否在已知可接受压缩内容的地址列表中。如果是这样,它将客户包装在`GzipSocket`装饰器中。注意根据配置和连接客户端,可以不使用或使用任意一个或使用全部装饰器。尝试使用多重继承来编写这个,看看你有多困惑! ### python中的装饰器 装饰器模式在Python中很有用,但是还有其他选择。例如,我们可以使用猴子修补,我们在第7章“Python面向对象的快捷方式”中讨论过,可以获得类似的效果。单一继承,“可选”计算在一个大型方法中完成,也是一个选项,多重继承不应该仅仅因为不适合前面看到的特定的例子而不被注销! </b> 在Python中,在函数上使用这种模式非常常见。正如我们在前面章节看到的,函数也是对象。事实上,函数装饰器是如此普遍,以至于Python提供了一种特殊的语法,使得将这种装饰器应用于函数变得容易。 </b> 例如,我们可以用一种更一般的眼光来看日志记录示例。除了仅仅记录套接字上调用`send`的日志,我们可能会发现记录所有对函数或方法的调用和很有用处。下面的示例实现了这个功能的装饰器: ``` import time def log_calls(func): def wrapper(*args, **kwargs): now = time.time() print("Calling {0} with {1} and {2}".format( func.__name__, args, kwargs)) return_value = func(*args, **kwargs) print("Executed {0} in {1}ms".format( func.__name__, time.time() - now)) return return_value return wrapper def test1(a,b,c): print("\ttest1 called") def test2(a,b): print("\ttest2 called") def test3(a,b): print("\ttest3 called") time.sleep(1) test1 = log_calls(test1) test2 = log_calls(test2) test3 = log_calls(test3) test1(1,2,3) test2(4,b=5) test3(6,7) ``` 这个装饰函数与我们之前探索的例子非常相似;在那些例子里,装饰器获取一个类似套接字的对象,并创建一个新的类似套接字的对象。这里,我们的装饰器获取一个函数对象并返回一个新的函数对象。这段代码由三个独立的任务组成: * 一个能接受另一个函数作为参数的函数`log_calls` * 该函数(内部)定义了一个新函数,名为`wrapper`,在调用原始函数之前做一些额外的工作 * 返回这个新函数 三个示例函数演示了如何使用装饰器。第三个函数包括调用一个睡眠方法以演示计时测试。我们将每个函数传递给装饰器,它返回一个新函数。我们将这个新函数赋予原始变量名称,高效地用修饰的函数替换原来的函数。 </b> 这个语法允许我们动态地构建修饰函数对象,就像我们在套接字示例所做的那样;如果我们不替换这个名字,我们甚至可以保留不同情况下的装饰和非装饰版本。 </b> 通常这些装饰器永久应用于不同的函数。在这种情况下,Python支持一种特殊的语法,在定义函数时应用装饰器。当我们讨论了属性装饰器时,我们已经看过这个语法;现在,让我们了解它是如何工作的。 除了在方法定义之后应用decorator函数,我们可以使用`@decorator`语法一次完成所有操作: ``` @log_calls def test1(a,b,c): print("\ttest1 called") ``` 这种语法的主要好处是,我们可以很容易地看到,函数已经在它被定义的时候被装饰了。如果装饰器后来被应用,读代码的人可能会忘记该函数已被更改。当回答诸如问题,“为什么我的程序日志函数调用出现在控制台上?”,会变得很困难!然而,该语法只能应用于我们定义函数时,因为我们无法访问其他模块的源代码。如果我们需要修饰其他人写的第三方库的一部分,我们必须使用早先的语法。 </b> 装饰器语法远比我们在这里看到的更多。我们不在这里涵盖高级主题,你可以查阅Python参考手册或其他教程了解更多信息。装饰器可以被创建作为可调用的对象,而不是只是返回函数的函数。类也可以被装饰;在这种情况下,装饰器返回一个新类,而不是一个新函数。最后,装饰器可以接受参数,在每个函数的基础上定制它们。 ## 观察者模式 观察者模式对于状态监控和事件处理情况非常有用。这种模式允许给定对象被未知动态“观察者”对象组监视。 </b> 每当核心对象上的值发生变化时,它都会通过调用`update()`方法让所有观察者对象知道,有一个变更发生了。每当核心对象发生变化时,每个观察者可以负责不同的任务;核心对象并不知道或不关心那些任务是什么,观察者通常也不知道或者不关心其他观察者在做什么。 </b> 下面是观察者模式的UML: ![](https://box.kancloud.cn/90050ede56d12c1492efa67ef65b7880_342x207.png) ### 观察者例子 观察者模式在冗余备份系统中可能很有用。我们可以写一个维护一些特定值的核心对象,然后一个或多个观察者创建核心对象的序列化副本。这些副本可能存储在远程主机的数据库中或本地文件中。让我们使用属性实现核心对象: ``` class Inventory: def __init__(self): self.observers = [] self._product = None self._quantity = 0 def attach(self, observer): self.observers.append(observer) @property def product(self): return self._product @product.setter def product(self, value): self._product = value self._update_observers() @property def quantity(self): return self._quantity @quantity.setter def quantity(self, value): self._quantity = value self._update_observers() def _update_observers(self): for observer in self.observers: observer() ``` 该对象有两个属性,当我们给它们设置后,将在属性上调用`_update_observers`方法。这个方法所做的就是历遍所有有效的观察者,让每个观察者知道有些事情已经改变了。在这种情况下,我们直接调用观察者对象;对象必须实现`__call__`方法来处理更新。这在许多面向对象编程语言中是不可能的,但是这是一个有用的快捷方式,有助于我们的代码更加易读。 </b> 现在让我们实现一个简单的观察者对象;它只是打印出一些状态到控制台: ``` class ConsoleObserver: def __init__(self, inventory): self.inventory = inventory def __call__(self): print(self.inventory.product) print(self.inventory.quantity) ``` 这里没有什么特别令人兴奋的;观察到的对象在初始化方法中被设置, 当观察者被调用时,我们会做“一些事情”,我们可以在一个交互式控制台测试这个观察者对象: ``` >>> i = Inventory() >>> c = ConsoleObserver(i) >>> i.attach(c) >>> i.product = "Widget" Widget 0 >>> i.quantity = 5 Widget 5 ``` 将观察者附加到`inventory`对象后,每当我们更改两个观察属性的一个属性,观察者都被调用,其动作被调用。我们甚至可以添加两个不同的观察者实例: ``` >>> i = Inventory() >>> c1 = ConsoleObserver(i) >>> c2 = ConsoleObserver(i) >>> i.attach(c1) >>> i.attach(c2) >>> i.product = "Gadget" Gadget 0 Gadget 0 ``` 这次我们更换产品名称时,有两组输出,每组对应一个观察者。这里的关键思想是,我们可以很容易地添加完全不同类型的观察者,同时备份文件、数据库或互联网应用程序中的数据。 </b> 观察者模式将被观察的代码与执行观察的代码分离。如果我们不使用这种模式,我们将不得不在每个属性安排代码处理可能出现的不同情况;登录控制台,更新数据库或文件,等等。每个任务的代码都与被观察对象混合在一起。维护它将是一场噩梦,稍后补充新的监控功能也会很痛苦。 ## 策略模式 策略模式是面向对象编程抽象的一个常见演示。该模式实现了对单个问题提供不同解决方案,每个都在不同的对象中。然后,客户端代码可以在运行时动态地选择最合适的方案。 </b> 通常,不同的算法有不同的权衡;一个可能比另一种快,但使用更多的内存,而当存在多个处理器或提供分布式系统时,第三种算法可能最合适的。策略模式的UML如下: ![](https://box.kancloud.cn/bba3d4471aae5f5d56c80f32d6e679b8_370x170.png) 连接到策略模式的用户代码只需要知道它正在处理抽象接口。选择的实际实现执行相同的任务,但方式不同;不管怎样,接口是相同的。 ### 策略例子 策略模式的典型例子是排序例程;这些年来,已经发明了许多算法来对对象集合进行排序;快排序、合并排序和堆排序都是具有不同特点的快速排序算法,根据输入的大小和类型、它们的输出顺序以及系统的要求,每一个算法都有自己的用处。 </b> 如果我们有需要对集合进行排序的客户端代码,我们可以将它传递给一个含有`sort()`方法的对象。该对象可以是`QuickSorter`或`MergeSorter`对象,但是这两种情况下的结果都是一样的:排序列表。排序策略是从调用代码中抽象出来的,使之模块化和可替换。 </b> 当然,在Python中,我们通常只调用`sorted`函数或`list.sort`方法,并相信它会以近乎最优的方式进行排序。所以,我们真的需要看一个更好的例子。 </b> 让我们考虑一个桌面壁纸管理器。当图像显示在桌面背景上,它可以以不同的方式适应屏幕大小。例如,假设图像比屏幕小,它可以平铺在屏幕上,以它为中心,或者按比例缩放。还有其他更复杂的策略可以使用,例如放大到最大高度或宽度,将其与纯色、半透明色或渐变背景色,或其他操作绑定。我们可能想稍后添加这些策略,让我们从基本策略开始。 </b> 我们的策略对象需要两个输入;要显示的图像,以及屏幕宽度和高度构成的元组。它们每一个根据给定的策略对图像进行处理,返回一个屏幕大小的新图像。你需要为本例使用`pip3`安装`pillow`模块: ``` from PIL import Image class TiledStrategy: def make_background(self, img_file, desktop_size): in_img = Image.open(img_file) out_img = Image.new('RGB', desktop_size) num_tiles = [ o // i + 1 for o, i in zip(out_img.size, in_img.size) ] for x in range(num_tiles[0]): for y in range(num_tiles[1]): out_img.paste( in_img, ( in_img.size[0] * x, in_img.size[1] * y, in_img.size[0] * (x+1), in_img.size[1] * (y+1) ) ) return out_img class CenteredStrategy: def make_background(self, img_file, desktop_size): in_img = Image.open(img_file) out_img = Image.new('RGB', desktop_size) left = (out_img.size[0] - in_img.size[0]) // 2 top = (out_img.size[1] - in_img.size[1]) // 2 out_img.paste( in_img, ( left, top, left+in_img.size[0], top + in_img.size[1] ) ) return out_img class ScaledStrategy: def make_background(self, img_file, desktop_size): in_img = Image.open(img_file) out_img = in_img.resize(desktop_size) return out_img ``` 这里我们有三种策略,每一种都使用`PIL`来完成它们的任务。单个 策略有一个`make_background`方法,接受相同的参数集。一旦选择后,可以调用适当的策略来创建正确大小的桌面图像。`TiledStrategy`遍输入图像的数量,根据图像的宽度和高度,将它们重复复制到新的位置。`CenteredStrategy`规划出需要在图像的四个边留出多少空间,使其居中。`ScaledStrategy`强制图像输出大小(忽略纵横比)。 </b> 考虑一下如何在没有策略模式下在这些选项中进行切换。我们需要将所有代码放入一个宏大的方法中,使用一个笨拙的`if`语句来选择预期的语句。每次我们想添加一个新的策略,我们必须使这个方法更加笨拙。 ### python中的策略 前面提到的策略模式的典型实现,虽然在大多数面向对象的库中非常常见,但在Python编程中已经很少见到。 </b> 这些类各自代表只提供一个单一函数的对象。我们可以很容易地调用函数`__call__`并直接调用对象。由于没有与对象相关联的其他数据,我们只需要创建一组顶级函数,并将其替代我们的策略进行传递。 </b> 因此,设计模式哲学的反对者会说,“因为Python已经有一流的函数,策略模式是不必要的”。事实上,Python一流的函数允许我们以更直接的方式实现策略模式。知道模式的存在仍然可以帮助我们为程序选择正确的设计,但是使用更可读的语法来实现它。策略模式,或它的顶层函数实现,当我们需要允许客户端代码或终端用户从同一接口的多个实现中进行选择时,才应该被使用。 ## 状态模式 状态模式在结构上类似于策略模式,但是它的意图和目的非常不同。状态模式的目标是表示状态转换系统:一个对象明显处于特定状态的系统,并且某些活动可能会将它推向不同的状态。 </b> 为了实现这一点,我们需要一个提供接口的管理器或上下文类,用于切换状态。在内部,这个类包含一个指向当前状态的指针;每个状态知道它被允许进入哪些其他状态,这些状态迁移取决于对其调用的操作。 </b> 所以我们有两种类型的类,上下文类和多个状态类。上下文类维护当前状态,以及去其他状态类的动作。状态类通常对调用上下文的其他对象是隐藏的;它就像一个黑匣子,碰巧在内部执行状态管理。下面是它在UML中的样子: ![](https://box.kancloud.cn/9c5a9581fd608b63c5fe184d53f8bf44_517x203.png) ### 状态例子 为了说明状态模式,让我们构建一个XML解析工具。上下文类将作为解析器本身。它将使用一个字符串作为输入,并将工具设置为初始解析状态。不同解析状态将吃掉字符,寻找特定的值,并且当找到该值时,更改为另外一个不同状态。目标是为每个标签及其内容创建一棵节点对象树。为了易于管理,我们将解析的XML的子集只有标签和标签名称。我们将无法处理上标签的属性。它将解析标签的文本内容,但不会尝试解析文本内部有标签的“混合”内容。下面是一个“简化的XML”文件的例子,我们将能够解析: ``` <book> <author>Dusty Phillips</author> <publisher>Packt Publishing</publisher> <title>Python 3 Object Oriented Programming</title> <content> <chapter> <number>1</number> <title>Object Oriented Design</title> </chapter> <chapter> <number>2</number> <title>Objects In Python</title> </chapter> </content> </book> ``` 在我们查看状态和解析器之前,让我们考虑一下这个程序的输出。我们知道我们想要一棵节点对象树,但是节点看起来像什么?显然它需要知道它正在解析的标签的名称,因为它是一棵树,它应该维护一个指向父节点的和该节点的顺序子节点列表的指针。有些节点有文本值,但不是全部。让我们先看看这个节点类: ``` class Node: def __init__(self, tag_name, parent=None): self.parent = parent self.tag_name = tag_name self.children = [] self.text="" def __str__(self): if self.text: return self.tag_name + ": " + self.text else: return self.tag_name ``` 该类在初始化时设置了默认属性值。`__str__`方法用来帮助我们完成时可视化树的结构。 </b> 现在,看看示例文档,我们需要考虑我们解析器可能在的状态。很明显,它将从一个没有节点被处理的状态开始。我们需要一个处理开始标签和结束标签的状态。当我们在一个带有文本内容的标签里,我们也必须把它作为一个单独的状态来处理。 </b> 切换状态可能很棘手;我们如何知道下一个节点是否是一个开始标记、结束标签,还是文本节点?我们可以在每个状态放一点逻辑来解决这个问题,但是创建一个新的状态更有意义,它唯一的目的是指示我们将切换到哪一个状态。如果我们称这种过渡状态为子节点,我们最终得到以下状态: * **FirstTag** * **ChildNode** * **OpenTag** * **CloseTag** * **Text** `FirstTag`状态将切换到`ChildNode`,然后`ChildNode`负责决定切换到其他三个状态中的哪一个;当这些状态结束时,它们会切换回到`ChildNode`。下面的状态转换图显示了可能的状态变化: ![](https://box.kancloud.cn/be6c8f955bbaf47bdb77d92f91fd3722_293x260.png) 状态负责收集“字符串的剩余部分”,尽它们所知去处理,然后告诉解析器处理其余的。让我们首先构造`Parser`类: ``` class Parser: def __init__(self, parse_string): self.parse_string = parse_string self.root = None self.current_node = None self.state = FirstTag() def process(self, remaining_string): remaining = self.state.process(remaining_string, self) if remaining: self.process(remaining) def start(self): self.process(self.parse_string) ``` 初始化方法在类中设置了一些各个状态将访问的变量。`parse_string`实例变量是我们试图解析的文本。根`root`节点是XML结构中的“顶部”节点。当前节点`current_node`实例变量是我们当前正在添加子变量的变量。 </b> 这个解析器的重要特性是`process`方法,它接受剩余的字符串,并将其传递到当前状态。解析器(`self`参数)也被传递到状态的`process`方法中,以便状态可以操作它。当状态完成处理,它将返回未解析的剩余字符串。然后解析器在剩余字符串上递归调用`process`方法,来构造树的其余部分。现在,让我们看看第一个`FirstTag`状态: ``` class FirstTag: def process(self, remaining_string, parser): i_start_tag = remaining_string.find('<') i_end_tag = remaining_string.find('>') tag_name = remaining_string[i_start_tag+1:i_end_tag] root = Node(tag_name) parser.root = parser.current_node = root parser.state = ChildNode() return remaining_string[i_end_tag+1:] ``` 这个状态发现第一个标签上打开和关闭角括号的索引(i_代表索引)。你可能认为这种状态是不必要的,因为XML需要开始标记前没有文本。然而,可能仍然会有空格;这就是为什么我们要搜索开角括号,而不是假设它是文档中的第一个字符。请注意,这段代码假设输入文件是有效的。一个正确的实现是严格测试无效输入,会试图恢复或显示一个描述清晰的错误消息。 </b> 方法提取标记的名称,并将其分配给解析器。它还将其分配给当前节点`current_node`,因为我们将要当前节点添加下一个子节点。 </b> 接下来是重要的部分:方法将解析器对象上的当前状态转换为子节点`ChildNode`状态。然后它返回字符串的剩余部分(在打开标签之后的)以允许它处理。 </b> 子节点`ChildNode`状态看起来相当复杂,结果却只是查询一个简单的条件: ``` class ChildNode: def process(self, remaining_string, parser): stripped = remaining_string.strip() if stripped.startswith("</"): parser.state = CloseTag() elif stripped.startswith("<"): parser.state = OpenTag() else: parser.state = TextNode() return stripped ``` `strip()`调用移除字符串中的空白。然后解析器确定下一个项目是开始或结束标签,或一串文本。取决于可能发生的情况,它将解析器设置为特定状态,然后告诉它解析字符串的剩余部分。 </b> `OpenTag`状态类似于`FirstTag`状态,只是它为上一个当前节点`current_node`对象的子节点`Child`上加了一个新创建的节点,并将其设置为新的当前节点`current_node`。在继续之前,它会将处理器重新置于子节点状态: ``` class OpenTag: def process(self, remaining_string, parser): i_start_tag = remaining_string.find('<') i_end_tag = remaining_string.find('>') tag_name = remaining_string[i_start_tag+1:i_end_tag] node = Node(tag_name, parser.current_node) parser.current_node.children.append(node) parser.current_node = node parser.state = ChildNode() return remaining_string[i_end_tag+1:] ``` `CloseTag`状态基本上是相反的;它设置解析器的当前节点`current_node`返回到父节点,这样外部标签中的任何其他子节点都可以添加到其中: ``` class CloseTag: def process(self, remaining_string, parser): i_start_tag = remaining_string.find('<') i_end_tag = remaining_string.find('>') assert remaining_string[i_start_tag+1] == "/" tag_name = remaining_string[i_start_tag+2:i_end_tag] assert tag_name == parser.current_node.tag_name parser.current_node = parser.current_node.parent parser.state = ChildNode() return remaining_string[i_end_tag+1:].strip() ``` 这两个断言`assert`语句有助于确保解析字符串一致。方法最后的`if`语句确保处理结束后处理器中止。如果节点的父节点是`None`,意味着我们正在根节点上工作。 </b> 最后,`TextNode`状态非常简单地提取下一个结束标记之前的文本 将其设置为当前节点上的值: ``` class TextNode: def process(self, remaining_string, parser): i_start_tag = remaining_string.find('<') text = remaining_string[:i_start_tag] parser.current_node.text = text parser.state = ChildNode() return remaining_string[i_start_tag:] ``` 现在我们只需要在我们创建的解析器对象上设置初始状态。初始状态是一个`FirstTag`对象,所以只需将以下内容添加到`__init__`方法中: ``` self.state = FirstTag() ``` 为了测试这个类,让我们添加一个主脚本,从命令行打开一个文件, 解析它,并打印节点: ``` if __name__ == "__main__": import sys with open(sys.argv[1]) as file: contents = file.read() p = Parser(contents) p.start() nodes = [p.root] while nodes: node = nodes.pop(0) print(node) nodes = node.children + nodes ``` 这段代码打开文件,加载内容,并解析结果。然后按顺序打印每个节点及其子节点。我们最初在节点类上添加的`__str__`方法负责格式化打印节点。如果我们在早期的例子上运行脚本,它输出如下树: ``` book author: Dusty Phillips publisher: Packt Publishing title: Python 3 Object Oriented Programming content chapter number: 1 title: Object Oriented Design chapter number: 2 title: Objects In Python ``` 将它与原始的简化的XML文档进行比较,解析器工作正常。 ### 状态VS策略 状态模式看起来非常类似于策略模式;事实上,两者的UML图是相同的。实现也是相同的;我们甚至可以把我们的状态写成一级函数,而不是把它们包装成对象,正如对策略模式的建议。 </b> 虽然这两种模式具有相同的结构,但它们解决完全不同的问题。策略模式用于在运行时选择算法;一般来说,对于特定的用例,将只选择其中一种算法。另一方面,状态模式被设计成允许在不同状态之间动态地切换,随着一些过程的发展。在代码中,主要区别在于策略模式通常不知道其他策略对象。在状态模式中,状态或上下文需要知道它可以切换到其他哪个状态。 ### 策略转协程 状态模式是状态转换问题的典型面向对象解决方案。然而,这种模式的语法相当冗长。你还记得我们在第9章“迭代器模式”中构建的正则表达式日志文件解析器?你可以通过将对象构造为协程得到类似的效果。这是一个伪装的状态转换问题。这种实现和定义在状态模型定义对象(或函数)实现之间的主要区别是协程解决方案允许我们在语言结构中编码更多的样板文件。这两个实现,两者都没有本质上的优势,但是你可以 发现对于给定的“可读”定义,协程更加可读(首先,你必须理解协程的语法!)。 ## 单例模式 单例模式是最有争议的模式之一;许多人指责它是一种“反模式”,一种应该避免而不是被提倡的模式。在Python中,如果有人使用单例模式,他们几乎肯定在做一些错的事情,可能是因为他们之前用的是更严格的编程语言。 </b> 那为什么还要讨论它呢?单例模式是所有设计模式中最著名的一种。它不仅在过度面向对象的语言中非常有用,而且也是传统面向对象编程的重要组成部分。更重要的是,单例模式背后的想法是有用的,即使我们在Python中完全可以用不同的方式实现这个想法。 </b> 单例模式背后的基本想法是只允许某些对象存在一个实例。通常,这个对象是一种管理者类,类似于我们在第5章“何时使用面向对象编程”中讨论的类。这样的对象经常需要被各种各样的其他对象引用,并将引用传递给那些需要它们的管理者对象的方法和构造函数中,这使得代码难以阅读。 </b> 相反,当使用单例时,这些对象请求的是来自这个类的管理者对象的唯一一个实例,所以不需要传递对它的引用。UML图没有完全描述它,但是这里UML仍然是完整的: ![](https://box.kancloud.cn/4dec96b95a1ee9d80bf1d4b30204c580_258x118.png) 在大多数编程环境中,通常让构造函数私有构建单例类(这样就没有人可以创建它的其他实例了),然后提供获取该单例的静态方法。此方法在第一次调用它时创建一个新实例,然后每次再次调用它时返回相同的实例。 ### 单例实现 Python没有私有构造函数,但是出于这个目的,它有一些更好的东西。我们可以使用`__new__`类方法来确保只有一个实例可以被创建: ``` class OneOnly: _singleton = None def __new__(cls, *args, **kwargs): if not cls._singleton: cls._singleton = super(OneOnly, cls ).__new__(cls, *args, **kwargs) return cls._singleton ``` 当调用`__new__`时,它通常会创建该类的一个新实例。当我们覆盖它,我们首先检查是否已经创建了单例;如果没有,我们调用`super`来创建这个单例。因此,每当我们调用`OneOnly`的构造函数时,我们总是得到完全相同的实例: ``` >>> o1 = OneOnly() >>> o2 = OneOnly() >>> o1 == o2 True >>> o1 <__main__.OneOnly object at 0xb71c008c> >>> o2 <__main__.OneOnly object at 0xb71c008c> ``` 这两个对象位于同一内存地址;因此,它们是相同的对象。这个特定的实现不是非常透明,因为我们并不清楚我们创建的是单例对象。如果我们调用构造函数,期待的是创建一个新的对象实例;在这种情况下,单例并不满足我们的要求。如果我们真的认为我们需要一个单例,也许在类上加上文档说明,可以缓解这个问题。 </b> 但我们不需要它。Python程序员不赞成强迫他们的代码用户进入一个特定的心态。我们可能认为一个类只需要一个实例,但是其他程序员可能有不同的想法。单例可能会干扰分布式计算、并行编程和自动化测试。在所有这些情况下,拥有一个特定对象的多个或可选实例会非常有用,即使“正常”操作可能永远不需要这么一个对象。 </b> 模块变量可以模拟单例。 </b> 通常,在Python中,使用模块变量模拟单例是足够的。它不像一个单例那样“安全”,因为人们在任何时候都可以重新分配这些变量,但是就像我们在第2章“Python中的对象”中讨论的私有变量一样,这在Python中是可以被接受的。如果某人有改变这些变量的原因,我们为什么要阻止他们?人们也不会停止实例化对象的多个实例,如果他们有这么做的正当理由,为什么要干涉呢? </b> 理想情况下,我们应该给他们一个访问“默认单例”值的机制,同时也允许他们在需要时创建其他实例。从技术上讲,它根本不是一个单例,它提供了类似单例行为的Python机制。 </b> 为了使用模块变量代替单例,我们实例化一个已经定义好的类的实例。我们可以使用单例来改进我们的状态模式来。我们可以创建一个始终可访问的模块变量,就不用每次当我们更改状态时需要创建一个新的对象: ``` class FirstTag: def process(self, remaining_string, parser): i_start_tag = remaining_string.find('<') i_end_tag = remaining_string.find('>') tag_name = remaining_string[i_start_tag+1:i_end_tag] root = Node(tag_name) parser.root = parser.current_node = root parser.state = child_node return remaining_string[i_end_tag+1:] class ChildNode: def process(self, remaining_string, parser): stripped = remaining_string.strip() if stripped.startswith("</"): parser.state = close_tag elif stripped.startswith("<"): parser.state = open_tag else: parser.state = text_node return stripped class OpenTag: def process(self, remaining_string, parser): i_start_tag = remaining_string.find('<') i_end_tag = remaining_string.find('>') tag_name = remaining_string[i_start_tag+1:i_end_tag] node = Node(tag_name, parser.current_node) parser.current_node.children.append(node) parser.current_node = node parser.state = child_node return remaining_string[i_end_tag+1:] class TextNode: def process(self, remaining_string, parser): i_start_tag = remaining_string.find('<') text = remaining_string[:i_start_tag] parser.current_node.text = text parser.state = child_node return remaining_string[i_start_tag:] class CloseTag: def process(self, remaining_string, parser): i_start_tag = remaining_string.find('<') i_end_tag = remaining_string.find('>') assert remaining_string[i_start_tag+1] == "/" tag_name = remaining_string[i_start_tag+2:i_end_tag] assert tag_name == parser.current_node.tag_name parser.current_node = parser.current_node.parent parser.state = child_node return remaining_string[i_end_tag+1:].strip() first_tag = FirstTag() child_node = ChildNode() text_node = TextNode() open_tag = OpenTag() close_tag = CloseTag() ``` 我们所做的只是创建可以重用的各种状态类的实例。注意我们如何在类中访问这些模块变量,甚至是在这些变量被定义之前?这是因为类内部的代码直到调用该方法才会被执行,此时,整个模块都已经被定义了。 </b> 本例的不同之处在于,我们没有去创建一组浪费内存、必须被垃圾收集的新实例,我们为每一个状态重用一个单一的状态对象。即使多个解析器同时运行,也只有这些状态类需要被使用。 </b> 当我们创建最初的状态解析器时,你可能想知道为什么我们没有在每个单独的状态下将解析器对象传递给`__init__`,而是像我们已经做的那样把它传递到过程方法中。当时的状态可能随后被作为`self.parser的引用。这是一个完全有效的状态模式的实现,但是它不允许利用单例模式。如果这些状态对象维护对解析器的一个引用,那么它们不能同时用于引用其他解析器。 </b> > 记住,这是两种不同的模式,目的不同;单例对有利于状态模式的实现并不意味着这两种模式是相关的。 ## 模板模式 模板模式对于删除重复代码非常有用;这是在第5章*何时使用面向对象编程*中讨论的**不要重复你自己**原则的一个实现。这种设计模式适用于:我们有几个不同的任务,它们有一些共同的、但不是全部的步骤。这些共同步骤在基类中实现,不同的步骤则在子类中被重写,以提供自定义行为。从某些方面来说,这像一个广义的策略模式,只是算法的相似部分使用一个基础类共享。下图是这种模式的UML图: ![](https://box.kancloud.cn/4cbb013a56c04b1a5941b3fbcd162a82_190x170.png) ### 模板例子 让我们创建一个汽车销售报告员作为例子。我们将销售记录存储在一个 SQLite 数据库表。SQLite 是一个简单的基于文件的数据库引擎,它允许我们使用 SQL 语法存储记录。Python 3在其标准库中包含 SQLite ,所以不需要额外的模块。 </b> 我们需要完成两个普通的任务: * 选择所有新车销售记录,并将它们以逗号分隔格式输出到屏幕上 * 输出所有销售人员及其总销售额的逗号分隔列表,并将它保存到可以导入电子表格的文件中 这些任务看起来完全不同,但是它们有一些共同的特点。两者都需要执行以下步骤: 1. 连接到数据库。 2. 构建一个对新车或总销售额的查询。 3. 发出查询。 4. 将结果格式化为逗号分隔的字符串。 5. 将数据输出到文件或电子邮件中。 这两个任务的查询构造和输出步骤不同,但是其余步骤是相同的。我们可以使用模板模式将公共步骤放置到基类中,在两个子类中定义不同步骤。在我们开始之前,让我们创建一个数据库,并使用一些 SQL 语句往数据库中放入一些数据: ``` import sqlite3 conn = sqlite3.connect("sales.db") conn.execute("CREATE TABLE Sales (salesperson text, " "amt currency, year integer, model text, new boolean)") conn.execute("INSERT INTO Sales values" " ('Tim', 16000, 2010, 'Honda Fit', 'true')") conn.execute("INSERT INTO Sales values" " ('Tim', 9000, 2006, 'Ford Focus', 'false')") conn.execute("INSERT INTO Sales values" " ('Gayle', 8000, 2004, 'Dodge Neon', 'false')") conn.execute("INSERT INTO Sales values" " ('Gayle', 28000, 2009, 'Ford Mustang', 'true')") conn.execute("INSERT INTO Sales values" " ('Gayle', 50000, 2010, 'Lincoln Navigator', 'true')") conn.execute("INSERT INTO Sales values" " ('Don', 20000, 2008, 'Toyota Prius', 'false')") conn.commit() conn.close() ``` 希望你即使不懂 SQL 也能看到这里发生了什么;我们已经创建了一个保存数据的表,并使用六条插入语句添加销售记录。数据存储在名为`sales.db`的文件中。现在我们有了一个可以用于开发模板模式的样本数据。 </b> 既然我们已经概述了模板必须执行的步骤,我们可以开始定义包含这些步骤的基类。每一步都有自己的方法(使得任何一个步骤都很容易地有选择地被重写),我们还有一个可以依次调用这些步骤的管理方法。这些方法没有任何内容,它们看起来是这样的: ``` class QueryTemplate: def connect(self): pass def construct_query(self): pass def do_query(self): pass def format_results(self): pass def output_results(self): pass def process_format(self): self.connect() self.construct_query() self.do_query() self.format_results() self.output_results() ``` `process_format`方法是外部客户调用的主要方法。它确保每个步骤都按顺序执行,但不关心该步骤是否在这个类或子类中实现。举个例子,我们知道两个类的三个方法将是相同的: ``` import sqlite3 class QueryTemplate: def connect(self): self.conn = sqlite3.connect("sales.db") def construct_query(self): raise NotImplementedError() def do_query(self): results = self.conn.execute(self.query) self.results = results.fetchall() def format_results(self): output = [] for row in self.results: row =[str(i) for i in row] output.append(", ".join(row)) self.formatted_results = "\n".join(output) def output_results(self): raise NotImplementedError() ``` 为了帮助实现子类,没有定义的两种方法引发`NotImplementedError`。这是在Python中指定抽象接口的常见方式,当抽象基类看起来太重时。这些方法可以具有空的实现(带pass),或者可以完全不使用。然而,抛出`NotImplementedError`有利于程序员理解,这个类将被子类化并且这些方法被重写;如果使用空方法或不定义方法,我们很难识别需要实现的需求,一旦我们忘记实现它们,则很难调试。 </b> 现在我们有了一个模板类来处理无聊的细节,但是它足够灵活,允许执行和格式化各种各样的查询。最棒的是,如果我们想要将我们的数据库引擎从SQLite更改为另一种数据库引擎(例如,py-postgresql),我们只需要在这个模板类中完成它,而不必修改我们可能已经编写的两(或两百)个子类。现在让我们来看看实际的类: ``` import datetime class NewVehiclesQuery(QueryTemplate): def construct_query(self): self.query = "select * from Sales where new='true'" def output_results(self): print(self.formatted_results) class UserGrossQuery(QueryTemplate): def construct_query(self): self.query = ("select salesperson, sum(amt) " + " from Sales group by salesperson") def output_results(self): filename = "gross_sales_{0}".format( datetime.date.today().strftime("%Y%m%d") ) with open(filename, 'w') as outfile: outfile.write(self.formatted_results) ``` 考虑到它们正在做的事情,这两个类实际上很短:连接到数据库,执行查询,格式化结果,并输出结果。超类负责重复的工作,但是让我们很容易定义那些因任务而异的步骤。此外,我们还可以轻松更改在基类中提供的步骤。例如,如果我们想输出其他东西,而不是逗号分隔的字符串(例如:要上传到网站的HTML报告),我们仍然可以重写`format_results`。 ## 摘要 本章举例详细讨论了几种常见的设计模式、UML图,以及讨论了Python和静态类型面向对象语言之间的差异。装饰器模式通常使用 Python更通用的修饰语法实现。观察者模式是一种有用的方法,可以将事件与对这些事件采取的行动分离开来。策略模式允许选择不同的算法来完成相同的任务。状态模式看起来相似,但被用来表示系统,该系统使用明确定义的行动在不同状态间切换。单例模式,流行于一些静态类型语言,在Python中,几乎总是反模式的。 </b> 下一章,我们将结束对设计模式的讨论。