《Python编程实战:运用设计模式、并发和程序库创建高质量程序》—— 2.7 代理模式-阿里云开发者社区

开发者社区> 华章出版社> 正文
登录阅读全文

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

简介:

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

2.7 代理模式

若想用一个对象来代表另一个对象,则可使用“代理模式”(Proxy Pattern)。《Design Patterns》一书举了四个用例。第一个用例是“远程代理”(remote proxy):用本地对象来代表远程对象。RPyC程序库就是个很好的例子,它可以在服务器端创建对象,并在一台或多台客户端中创建针对这些对象的代理(6.2节将会介绍这个程序库)。第二个用例是“虚代理”(virtual proxy),用来创建能够代表复杂对象的轻量级对象,只在确有必要时才会真正去创建那个复杂对象。本节所举的例子就是这种代理。第三个用例是“保护代理”(protection proxy),可根据客户端的访问权限来确定不同的访问级别。最后一种用例是“智能引用”(smart reference),可用来在“访问对象时执行额外操作”(performs additional actions when an object is accessed)。这些代理模式都可以采用同一套编码方式来实现,其中第四种代理还可以通过描述符来实现(比方说,利用@property修饰器,以属性来取代普通对象)。
代理模式也可用于单元测试。例如,受测代码所需访问的资源并非随时可用,或是所需使用的类尚未开发完毕而依然不完整,那就可以考虑为资源或类创建代理,令代理对象提供所有接口,并且用“桩”(stub)来表示那些缺失的功能。这种做法非常有用,Python 3.3包含了unittest.mock库,可用来创建“模拟对象”(mock object),并设置“桩”来表示缺失的方法。
本节范例所假定的使用场景是:我们可能会创建很多图像,但最后只会用到其中一张。Image模块与功能相仿但速度更快的cyImage模块都可以创建图像(第3.12节及5.3节分别讲解二者),但它们一开始就会把图像创建在内存里。而我们只会用到这些图像中的一张,所以更好一些的办法是:创建许多轻量级图像代理,然后只在真正有需要时才去创建实际图像。
除了构造器之外,Image.Image类的接口还有十个方法:load()、save()、pixel()、set_pixel()、line()、rectangle()、ellipse()、size()、subsample()、scale()。(此外,还有一些静态的便捷方法以及等效的模块函数,例如Image.Image.color_for_name()及Image.color_for_name()。)
代理类只需要实现Image.Image中我们必须用到的那些方法即可。首先来看代理类的用法。本节范例代码选自imageproxy1.py,绘制出的图像如图2.8所示。

b41289dcf127774f1b5e971a0e6fbef31ccf5a92

首先,需要用Image模块的color_for_name()函数创建一些颜色常量。
screenshot

上面这段代码先创建了ImageProxy对象,我们在创建时把需要使用的Image类传给了构造器。然后又在对象上面绘制了一些内容,最后将绘制好的图像存储起来。假如创建图像时调用的不是ImageProxy()而是Image.Image(),那么剩下的绘制操作依然能照常执行。但是,采用了图像代理之后,只有在调用save()方法时才会去创建真正的图像,这样的话,在执行保存操作之前,创建图像的开销(无论是内存开销还是处理开销)就变得非常小,若是最后不保存图像而直接将其丢弃,那么损失也会很低。若用Image.Image来创建,则一开始就需要很大开销(也就是说,一开始就要创建大小为width×height的数组用以保存颜色值),而且在绘制时还需要执行很多处理工作(例如在填充矩形时,要计算出需要填充的像素,并把它们的颜色都设置好),即便最后决定要丢弃这张图像,也还是得执行丢弃之前那些操作。
screenshot

只要ImageProxy所提供的接口足够用,它就能代表Image.Image(如果构造时传入了支持Image接口的其他类,那么也能代表那个类)。ImageProxy并不保存图像,它保存的是一份元组列表,每个元组表示一条命令,其首个元素是函数或非绑定方法,其余元素是传给调用函数或方法的参数。
创建ImageProxy对象时,必须指定长和宽(以便按此大小来新建图像)或文件名。如果用文件名创建ImageProxy,那么就会保存一条命令,这条命令旨在调用Image.Image()构造器,构造器所用的width及height参数都是None,而filename参数则是创建ImageProxy时所传入的文件名,ImageProxy.load()方法所对应的命令与此相同。创建好ImageProxy对象之后,如果又调用了ImageProxy.load()方法,那么先前的全部命令都将丢弃,self.commands命令列表中只会留下一条新建图像的命令。若用给定的长度与宽度来创建ImageProxy对象,则对应的命令中保存的是Image.Image()构造器,构造器所用的width及height参数是创建时所传入的长度与宽度。
如果调用了代理对象所不支持的方法(比如pixel()),那么Python就会发现这个方法找不到,从而自动抛出AttributeError,而这正是我们想要的效果。还有一种处理办法:如果代理对象不支持将要调用的方法,那就把实际的Image对象创建出来,并在此对象上执行后续操作。(imageproxy2.py程序采用这种办法,该程序的代码没有列在本节中。)
screenshot
screenshot

Image.Image类的接口中有四个绘制方法:line()、rectangle()、ellipse()、set_pixel()。我们的ImageProxy类完全支持这些方法,但并不当场执行操作,而是把操作及其参数做成一条命令,放在self.commands列表里。
screenshot

只有在保存时才需要创建真正的图像,也只有此时才会有真正的处理开销及内存开销。ImageProxy的设计方式决定了其首个命令一定是新建图像(可能是根据长宽来创建,也可能是从既有文件中加载)。所以我们采用特殊方式来处理第一条命令:将执行该命令所得的返回值保存起来,这个返回值肯定是个Image.Image或cyImage.Image。然后,遍历剩下的命令,并依次执行之,由于执行的都是非绑定方法,所以需要把image变量作为首个参数(也就是self)传进去。最后,调用Image.Image.save()方法,保存图像。
虽说Image.Image.save()方法在发生错误时会抛出异常,但这个方法本身是没有返回值的。然而ImageProxy的save()方法却稍有不同,它会把创建好的Image.Image对象返回给调用者,以备后续处理时所需。这样修改应该不会出问题,因为假如调用者不使用返回值的话(比如调用Image.Image.save()方法时,我们就没打算使用返回值),那么Python就会将其直接丢弃。imageproxy2.py程序无须像这样修改,因为它有个类型为Image.Image的image属性可供访问,如果访问时图像尚未创建,那么会当场创建一份。
像本例这样把命令存储起来,可以为实现“执行-撤销”(do-undo)功能做准备,这一话题请参考3.2节的命令模式以及3.8节的状态模式。
结构型设计模式都可以用Python语言实现出来。适配器模式与外观模式能够把已有的类放在新环境下重新使用,而桥接模式则可以把某个类里的复杂功能嵌入另一个类中。组合模式可以非常方便地创建出对象层次结构,但Python中却很少用到它,因为采用dict就可以实现相同的功能了。修饰器模式特别有用,Python语言对此提供了原生支持,我们还可以用修饰器来修饰类。Python的对象引用机制可以视为享元模式的变种。代理模式在Python中实现起来非常简单。设计模式不仅可用于创建各种简单及复杂的对象,而且还能指导对象的行为,也就是规定单个对象或一组对象应该怎样完成其工作。下一章就要讲解这些“行为型设计模式”。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享: