赞
踩
本节书摘来自华章出版社《Python编程实战:运用设计模式、并发和程序库创建高质量程序》一 书中的第1章,第1.3节,作者:(美) Mark Summerfield,更多章节内容可以访问云栖社区“华章计算机”公众号查看。
如果子类的某个方法要根据情况来决定用什么类去实例化相关对象,那么可以考虑工厂方法模式。此模式可单独使用,也可在无法预知对象类型时使用(比方说,待初始化的对象类型要从文件中读入,或是由用户来输入)。
本节编写一段棋盘生成程序,用以生成“国际跳棋”(checker)和“国际象棋”(chess)的棋盘。该程序所输出的两张棋盘如图1.3所示。这段程序有四个版本,其源代码分别存放在gameboard1.py至gameboard4.py中。
我们先设计出抽象的棋盘类,然后用其子类创建特定的棋盘。每个子类都会生成相应的棋盘,并把棋子摆放好。每个棋子也有对应的类(比如黑色的跳棋棋子用BlackDraught类表示,白色的跳棋棋子用WhiteDraught类表示,黑色的“象”用BlackChessBishop表示,白色的“马”用WhiteChessKnight表示)。为了和Unicode中的字符名称保持一致,我们在表示跳棋棋子时使用了Draught一词,而没有使用Checker,比如白色的跳棋棋子叫做WhiteDraught,而不叫WhiteChecker。
我们打算先讲最顶层的代码,这部分代码用于实例化棋盘对象,并把棋盘打印到控制台。然后来看表示棋盘的类和表示棋子的类。一开始,我们采用“硬代码”类的方式来创建这些棋子。然后设法将其改写,去掉那些硬代码类,并缩减代码行数。
四个版本的程序都要用到上面这个函数。该函数分别创建两种棋盘对象,并将其打印到控制台,打印的时候会调用AbstractBoard的__str__()方法,以便将棋盘对象的内容转换成字符串。
BLACK与WHITE常量表示棋盘格子的背景色。在后续版本中,还会用来表示棋子的颜色。上面这段代码是从gameboard1.py中节录的,其他三个版本也与此相同。
BLACK, WHITE = ("BLACK", "WHITE")这行代码本来可以按惯例写成BLACK, WHITE = range(2)。但是用字符串来定义常量在调试时更容易看出错误信息的含义,而且Python还会自动把内容相同的字符串规整起来,只保留一份。
棋盘对象里包含一份二维列表,其中每个一维列表都表示棋盘中的一行,而一维列表中的元素则表示行中对应单元格上的棋子,如果某个格子上没有棋子,那么对应的元素就是None。console()函数(此函数没有出现在上述程序清单中)所返回的字符串用于表示棋子及其背景色。(在“类Unix”系统中,console()函数所返回的字符串里会包含转义符,用于修改字符的背景色。)
你可以把AbstractBoard类的metaclass设置成abc.ABCMeta(1.2节中的AbstractFormBuilder类就是如此),这样的话,它就成了真正的抽象基类。不过此处我们改用另一种做法:凡是需要由子类重新实现的方法都抛出NotImplementedError异常。
上述子类用于创建10×10的国际跳棋棋盘。该类的populate_board()目前还算不上工厂方法,因为它是用硬编码的类来实例化棋子对象的,稍后我们会以此为基础将之改写成工厂方法。
在gameboard1.py这一版程序中,ChessBoard类的populate_board()方法与CheckersBoard类的同名方法一样,都不能称为工厂方法,不过,由上面这段代码我们可以看出国际象棋的棋盘是如何生成的。
上面这个Piece类是所有棋子的基类,本来也可以直接用str表示,但如果那样做的话,就没办法判断某个对象是不是棋子了(比如我们想用isinstance(x, Piece)来判断x对象是不是棋子)。__slots__ = ()这行语句可以保证实例中不会有任何数据,我们把这个话题放在2.6节中讨论。
上面这两个类是所有棋子的范本。每种棋子所对应的类都是Piece的子类,而Piece本身又是str的子类,棋子对象都是“不可变的”(immutable),我们用与之对应的Unicode字符来初始化它。__new__()中所用的Unicode字符其模样与对应的棋子相同。总共有14个这样的子类,它们都非常相似,只是类名与所含字符串不同,最好能把这些近乎重复的代码清理掉。
上面列出了从gameboard2.py中节选的新版CheckersBoard.populate_board()方法,这次就可以把它叫做工厂方法了,因为此方法会用名为create_piece()的“工厂函数”(factory function)来创建棋子,而不像以前那样直接用硬编码的棋子名称来创建。create_piece()函数会根据其参数返回适当类型的对象(比方说,如果color是"black",那就创建BlackDraught对象;如果color是"white",则创建WhiteDraught对象)。新版代码中的ChessBoard.populate_board()方法(并未列在上述程序清单中)与之类似,也会调用其create_piece()函数,根据棋子颜色及名称来创建相应的对象。
工厂函数使用Python语言内置的eval()函数来创建对应类的实例。比方说,如果参数是"knight"与"black",那么交由eval()函数执行的字符串就是"BlackChessKnight()"。虽说这样做完全可行,但可能会有风险,因为任何字符串都会当成Python代码交给eval()函数执行。稍后我们将换用另一种办法,以Python语言内置的type()函数来创建实例。
这次不需要再把14个相似的类逐个写出来了,而是以一段代码块为模板,将其全都创建好。
调用itertools.chain()函数时,可传入一个或多个iterable(可迭代物,可遍历物),此函数将返回另外一个iterable,在返回那个的iterable上面遍历时,会先遍历刚才调用时传入的首个iterable,然后再遍历刚才调用时传入的第二个iterable,依此类推。本例中,我们给函数传了两个iterable,第一个iterable是个二元组(2-tuple),其中的两个值分别是黑色与白色跳棋棋子的“Unicode码位”(Unicode code point),而第二个iterable则是个range对象(实际上就是个生成器),用于指定各种黑色与白色的国际象棋棋子。
对于每个码位来说,我们都创建一个仅包含该字符的字符串(比如“”),并根据其“Unicode名称”(Unicode name)来确定类名(例如,黑色“马”的Unicode名称是“black chess knight”,所以创建出来的类就叫做BlackChessKnight)。确定了字符与类名之后,就可以用exec()来创建所需的类了。原来那版程序需要用100多行代码来逐个创建这些类,而现在只用十几行就够了。
但是,用exec()所带来的风险比用eval()还要高,所以必须得找个更好的办法才行。
上面这个CheckersBoard.populate_board()方法是从gameboard3.py中节选的。与前一版相比,这个版本的棋子与颜色用的都是常量,而不是“字符串字面值”(string literal),因为那样很容易打错字,而且这一版采用新的create_piece()方法来创建棋子。
gameboard4.py程序将“列表推导”(list comprehension)技术与两个itertools函数结合起来,用另一种办法实现了CheckersBoard.populate_board()函数(此函数没有列在上述程序清单里)。
在这一版程序(也就是gameboard3.py)中,create_piece()工厂函数是AbstractBoard类的方法,CheckersBoard与ChessBoard类都会继承它。该方法接受两个常量做参数,根据棋子种类及颜色在静态的(也就是类级别的)字典中找到对应的类,这个字典的键是(piece kind, color)二元组,值是“类对象”(class object)。找到值(也就是所需的类)之后,立即用()操作符将其实例化,并返回创建好的棋子对象。
字典中的类本来可以像gameboard1.py那样直接写成硬代码,或是像gameboard2.py那样用不太安全的办法动态创建出来,但在gameboard3.py中,我们要采用一种较为安全的办法来做:这次仍然是动态创建,只是不再使用eval()与exec()了。
上面这段代码的结构与早前动态创建14个子类的那段代码基本相同,只是这次没使用eval()与exec(),而是改用了一种较为安全的办法。
知道了与棋子相对应的字符及类名之后,就可以用自定义的make_new_method()函数来创建new()函数了。创建好new()函数后,再用Python内置的type()函数创建新的类。以这种方式创建类的时候,必须传入类型名称、含有基类名称的元组(在本例中,只有一个基类,就是Piece)以及含有类属性的字典。在字典中,我们将__slots__属性设为空元组(这样的话,在类的实例中就不会出现私有的__dict__了),并将__new__“方法属性”(method attribute)设置成刚才创建好的new()函数。
最后,调用内置的setattr()函数,把新创建的类(用Class变量表示)当作属性(属性名用name变量表示,比方说,在创建白色的“兵”时,name变量的值就是"WhiteChessPawn")添加到当前模块(sys.modules[__name__])中。gameboard4.py用更为简洁的方式改写了上述程序清单中的最后一行代码:
上面这种写法的意思是:在存放全局变量的dict里添加元素,新元素的键是name,值是刚才创建的Class。这种写法的效果与gameboard3.py中的setattr()那行语句完全一样。
上面这个函数是用来创建new()函数的(而创建好的函数将成为类的__new__()方法)。在创建new()函数时不能调用super(),因为此处并没有super()函数所需的“类环境”。请注意,尽管Piece类没有__new__()方法,但其基类str有,所以make_new_method()函数所调用的Piece.__new__()实际上指的是str.__new__()。
前面代码中的new = make_new_method(char)语句以及make_new_method()函数其实都可以删掉,把原来调用make_new_method()函数的代码改成下面这两行语句就好:
上面这段代码先写了lambda表达式,然后立刻用char来填充外围的lambda,以此创建出new()函数。(gameboard4.py用的就是这种写法。)
所有的lambda函数都叫做"lambda",这在调试的时候不易区分,所以创建好new()函数之后,我们又给它起了个新名字。
为了使范例代码完整一些,笔者在上面列出了ChessBoard.populate_board()方法的代码,gameboard3.py及gameboard4.py都使用此方法。它用棋子颜色及棋子类型常量来生成棋盘(也可以不写成硬代码,而是从文件中读入,或令用户通过菜单来选择)。gameboard3.py使用的是早前列出的那个create_piece()工厂函数,而gameboard4.py所使用的create_piece()则是最终版。
上面是gameboard4.py的create_piece()工厂函数,其所用的常量与gameboard3.py相同,但它并没有专门把类对象保存到字典中,而是调用内置的globals()函数,在返回的全局变量字典里查出所需的类对象,立刻将其实例化,并返回创建好的棋子对象。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。