《Python编程实战:运用设计模式、并发和程序库创建高质量程序》—— 2.3 组合模式

简介:

本节书摘来自华章出版社《Python编程实战:运用设计模式、并发和程序库创建高质量程序》一 书中的第2章,第2.3节,作者:(美) Mark Summerfield,更多章节内容可以访问云栖社区“华章计算机”公众号查看。

2.3 组合模式

“组合模式”(Composite Pattern)可用来统合类体系中的两种对象:一种对象能够包含体系中的其他对象,另一种不能。前者叫做“组合体”(composite),后者叫做“非组合体”(noncomposite),两者统称“组件”(component)。按照传统的实现方式,这两种组件(一种是单个对象,一种是对象群集)所对应的类都继承自同一个基类。组合体与非组合体对象都具备同一套“核心方法”(core method),此外,组合体对象还有用于增加、移除、遍历子对象的其他方法。
该模式常用于实现Inkscape等绘图程序,这种程序需要有“群组”(group)与“解除群组”(ungroup)功能。用户可选取一批组件,并对其执行群组或解除群组操作,而这些组件中,有的是单个元素(比如矩形),有的是组合体(比如由各种图形所构成的脸谱)。
现在就来看个实际的例子。我们在main()函数里创建一些对象,有单个元素,也有组合体,然后,把它们全都打印出来。下面这段代码选自stationery1.py,代码后面是程序所输出的信息。
screenshot
screenshot

每个SimpleItem对象都有名称及价格,CompositeItem对象也有名称,而且可以包含任意数量的SimpleItem或CompositeItem,也就是说,组合体可以无限嵌套。组合体的价格是其全部元素的价格之和。
在本例中,“铅笔套件”(pencil set)包含一只“铅笔”(pencil)、一把“尺子”(ruler)、一块“橡皮”(eraser)。而“盒装铅笔套件”(boxed pencil set)则包含“文具盒”(box)、铅笔套件及另一只铅笔。图2.4演示了盒装铅笔套件与其元素之间的关系。

773147f1162621dbf3c0121347ad9bb522969fca

接下来,我们要以两种方法实现组合模式,第一种是传统做法,第二种是用一个类来表示组合体与非组合体。

2.3.1 常规的“组合体/非组合体”式层级

在常规的实现方式中,所有组件(无论是组合体还是非组合体)都具有相同的抽象基类AbstractItem,而且组合体要直接继承自另外一个抽象基类AbstractCompositeItem。整个类体系如图2.5所示。我们先看AbstractItem这个基类。
screenshot

我们要求所有子类的对象都能向用户汇报自己是不是组合体,同时还要求子类对象必须可以迭代,__iter__()方法的默认行为是返回一个“迭代器”(iterator),该迭代器会在空序列上迭代。
由于AbstractItem类至少有一个抽象方法或抽象属性,所以我们无法创建此类的对象。(从Python 3.3版本开始,也可以把@abstractproperty def method(...): ...写成@property @abstractmethod def method(...): ...。)
screenshot

SimpleItem类用来表示非组合体。在本例中,每个SimpleItem对象都有name及price属性。
由于SimpleItem继承了AbstractItem,所以它必须重新实现基类的全部抽象属性及抽象方法,具体到本例,也就是要实现composite属性。由于AbstractItem类的__iter__()方法不是抽象的,所以无须重新实现,基类的代码会返回指向空序列的迭代器,子类沿用这个实现即可。这样做是合理的,因为SimpleItem对象不是组合体,所以返回空迭代器可以令我们把SimpleItem与CompositeItem对象统合起来(至少在迭代时是如此)。比方说,可以把由这两种对象混合而成的对象交给itertools.chain()来迭代。
screenshot

为了便于打印信息,我们给组合体及非组合体都定义了print()方法,打印时,缩进宽度会随着嵌套深度而加大。
screenshot

上面这个类是CompositeItem的基类,它实现了组合体所需的添加、移除和迭代等功能。由于该类从AbstractItem中继承了抽象的composite属性但却没提供实现,所以无法实例化。
screenshot

上述方法接受若干个item(既可以是SimpleItem,也可以是CompositeItem),并将其加入本组合体的子对象列表中。编写该方法时,不能去掉first参数而只保留items,因为假如那样做的话,items所捕获的元素数量就可能为0,虽然无害,但却会把用户在代码中所犯的逻辑错误掩盖掉。(item的用法请参阅1.2节的补充知识中所讲的解包操作。)另外,此函数没有禁止“循环引用”(circular reference),比方说,用户可通过add()方法把某个组合体对象设置成其自身的子对象。
在下一小节中,我们将看到另一种方法:只需一行代码即可实现add()方法。
screenshot

我们用了个简单的办法来实现remove()方法,一次只移除一个item。如果要移除的item是组合体,那么其下各个层级中的子对象也会一并从体系里移除。
screenshot

实现了__iter__()这个特殊方法之后,就可以在for循环、“列表推导”(comprehension)及生成器里遍历组合体对象的子对象了。本来也可以把方法体写成for item in self.children: yield item,但由于self.children是个序列(也就是列表),因此直接用Python内置的iter()函数来实现更为简单。
screenshot

上面这个CompositeItem类用来表示具体的组合体对象,它有自己的name属性,但与组合体相关的其他任务(也就是子对象的增加、移除、迭代操作)都交由基类处理。由于本类已经实现了抽象的composite属性,而且并未留下其他尚待实现的抽象属性或抽象方法,所以CompositeItem可以实例化。
screenshot

price是个“只读属性”(read-only property),其代码稍微有点难懂。这行代码构建了一条“生成器表达式”(generator expression),并用内置的sum()函数来计算组合体中所有子对象的价格,如果子对象也是组合体,那就递归计算下去。
for item in self表达式使得Python调用iter(self)来获取针对self的迭代器,而这又会调用__iter__()特殊方法,该方法返回指向self.children的迭代器。
screenshot

为了便于打印信息,本类也提供了print()方法,其首行代码与SimpleItem类的print()方法重复了。
本例中的SimpleItem与CompositeItem能够应对绝大多数情况。但若要构建更为精细的层次结构,则可以从这两个类或其抽象基类中继承专门的子类。
AbstractItem、SimpleItem、AbstractCompositeItem、CompositeItem这四个类确实搭配得很好,但代码稍显冗长,而且接口也不统一:组合体有add()及remove()方法,非组合体却没有。下一小节我们就来解决这些问题。

2.3.2 只用一个类来表示组合体与非组合体

上一小节的四个类(两个抽象类,两个具体类)似乎有些多了,而且接口也没有完全统一:只有组合体才支持add()及remove()方法。如果能忍受少许额外开销的话,我们可以只用一个类来表示组合体与非组合体:给这两种对象都配备一份列表及一个float型属性,非组合体对象里的列表是空的,而组合体对象里的float型属性并不使用。此方案所设计出来的对象其行为更加合理,因为两种对象的接口完全一致:非组合体与组合体一样,也有add()及remove()方法。
本节将新建Item类,组合体与非组合体都可以用这个类表示,无须再借助其他类。本节的范例代码摘录自stationery2.py文件。
screenshot

__init__()方法的参数不太整齐,但是没关系,我们稍后就会看到,用户实际上无须手工调用Item()来创建对象。
每个对象都必须有名字,而且还必须有价格,构建对象的时候,若未指定价格,则会使用默认值。此外,构建对象时还可以通过*items参数放入零个或多个子对象,这些子对象将保存在self.children里面。非组合体对象的children是个空列表。
screenshot
screenshot

上面这两个工厂方法都是类方法,它们的参数也都比Item.__init__()整齐,二者均可非常方便地创建Item对象。有了这两个方法之后,SimpleItem("Ruler", 1.60)与CompositeItem("Pencil Set", pencil, ruler, eraser)可分别改写为Item.create("Ruler", 1.60)及Item.compose("Pencil Set", pencil, ruler, eraser)。而且上一小节的四个类现在都合并成Item类型了。当然,用户如果愿意,也可以直接用Item()来创建对象,比如:Item("Ruler", price=1.60)、Item("Pencil Set", pencil, ruler, eraser)。
screenshot

我们还提供了上面这两个工厂函数,其作用与刚才提到的那两个工厂方法相同。在使用模块时,这种工厂函数更为便利。例如,如果Item类在Item.py模块中,那么有了这两个工厂函数之后,我们就不用再写Item.Item.create("Ruler", 1.60)了,而是可以写成Item.make_item("Ruler", 1.60)。
screenshot

composite属性的实现方式与原来不同,因为有些Item对象是组合体,有些则不是。如果Item的self.children列表非空,那么我们就认定此对象是组合体。
screenshot

add()方法的实现代码与上一小节稍有不同,这次用的办法应该会更加高效一些。itertools.chain()函数接受若干个iterable,并返回一个iterable,在返回的iterable上面迭代,其效果就等于依次在参数里的各个iterable上面迭代。
无论对象是不是组合体,都可以在它上面调用add()方法。若在非组合体上调用add()方法,则会令其变为组合体。
把非组合体变为组合体时,会产生一个小问题:由于price属性现在表示所有子对象的总价格,所以该对象本身的价格反而看不到了。若想保留自身价格,当然也有其他办法可循。
screenshot

如果把组合体最后一个元素移除,那么它就变成了非组合体。这样做的效果是:本对象的价格不再是其所有子对象的价格总和了(这些子对象现在没有了),而会等于其私有的self.__price属性。为了确保相关逻辑正确,我们在__init__()方法里为所有对象都设置了初始价格。
screenshot

在组合体上调用__iter__()方法会返回其子对象列表,在非组合体上调用,会返回空序列。
screenshot

price属性必须同时适用于组合体及非组合体。对于前者来说,它表示其子对象的价格总和,对于后者来说,它表示本对象的价格。
screenshot

上面这个方法和price属性一样,也必须对组合体及非组合体都适用才行,此方法的代码与上一小节的CompositeItem.print()方法相同。如果在非组合体上执行print()方法,那么执行到for语句时,该对象就会返回指向空序列的迭代器,这样的话,遍历时就不用担心“无限递归”(infinite recursion)问题了。
由于Python语言很灵活,所以用它来创建组合体与非组合体是件很简单的事:想缩减存储开销时,可以分别建立两个类,而若要提供完全统一的接口,则可以合并成一个类。
3.2节讲述“命令模式”(Command Pattern)时,将会谈到组合模式的另一种变化形式。

相关文章
|
22小时前
|
Java 测试技术 Python
Python的多线程允许在同一进程中并发执行任务
【5月更文挑战第17天】Python的多线程允许在同一进程中并发执行任务。示例1展示了创建5个线程打印"Hello World",每个线程调用同一函数并使用`join()`等待所有线程完成。示例2使用`ThreadPoolExecutor`下载网页,创建线程池处理多个URL,打印出每个网页的大小。Python多线程还可用于线程间通信和同步,如使用Queue和Lock。
12 1
|
2天前
|
设计模式 Java
【设计模式】文件目录管理是组合模式吗?
【设计模式】文件目录管理是组合模式吗?
7 0
|
2天前
|
Python
【Python进阶(二)】——程序调试方法
【Python进阶(二)】——程序调试方法
|
2天前
|
Python
Python的全局变量作用于整个程序,生命周期与程序相同,而局部变量仅限函数内部使用,随函数执行结束而销毁。
【5月更文挑战第11天】Python的全局变量作用于整个程序,生命周期与程序相同,而局部变量仅限函数内部使用,随函数执行结束而销毁。在函数内部修改全局变量需用`global`关键字声明,否则会创建新局部变量。
104 2
|
2天前
|
消息中间件 程序员 调度
Python并发编程:利用多线程提升程序性能
本文探讨了Python中的并发编程技术,重点介绍了如何利用多线程提升程序性能。通过分析多线程的原理和实现方式,以及线程间的通信和同步方法,读者可以了解如何在Python中编写高效的并发程序,提升程序的执行效率和响应速度。
|
2天前
|
缓存 Shell 开发工具
[oeasy]python0016_在vim中直接运行python程序
在 Vim 编辑器中,可以通过`:!`命令来执行外部程序,例如`:!python3 oeasy.py`来运行Python程序。如果想在不退出Vim的情况下运行当前编辑的Python文件,可以使用`%`符号代表当前文件名,所以`:!python3 %`同样能运行程序。此外,可以使用`|`符号连续执行命令,例如`:w|!python3 %`会先保存文件(`w`)然后运行Python程序。这样,就可以在不离开Vim的情况下完成编辑、保存和运行Python程序的流程。
19 0
|
2天前
|
监控 开发者 Python
Python中记录程序报错信息的实践指南
Python中记录程序报错信息的实践指南
17 1
|
2天前
|
监控 测试技术 持续交付
Python自动化测试代理程序可用性
总之,通过编写测试用例、自动化测试和设置监控系统,您可以确保Python自动化测试代理程序的可用性,并及时发现和解决问题。这有助于提供更可靠和高性能的代理服务。
17 4
|
2天前
|
监控 测试技术 API
Python Web应用程序构建
【4月更文挑战第11天】Python Web开发涉及多种框架,如Django、Flask和FastAPI,选择合适框架是成功的关键。示例展示了使用Flask创建简单Web应用,以及如何使用ORM(如SQLAlchemy)管理数据库。
59260 4
|
2天前
|
人工智能 数据库 开发者
Python中的atexit模块:优雅地处理程序退出
Python中的atexit模块:优雅地处理程序退出
19 3