2.2节中实现井字棋游戏的代码体现了一种面向过程的编程设计思路。例如,从接收玩家输入到判断胜负,再到显示新的棋盘,将整体游戏逻辑根据相应过程拆开,然后分别用函数实现每个过程的逻辑。当程序功能非常简单时,基于过程的设计思路没什么问题。但当程序功能越来越复杂时,我们需要一种更灵活的设计思路,也就是面向对象编程,其英文名为Object-Oriented Programming,通常缩写为OOP。
面向对象编程的主要观点是不应该将程序拆分为若干过程,而应该将其拆分为自然对象的模型。面向对象编程涉及几个关键概念,包括类、对象、组件、属性和行为。我们分别来解释这些关键概念。类可以看作一张蓝图或图纸,代表了抽象概念,而对象是具体的事物。类和对象的关系是抽象和具体的关系,就好比汉字“马”所代表的抽象概念,和一匹正在草原上奔跑的骏马所代表的具体事物的关系。战国时期所说的“白马非马”的故事,就包含抽象和具体的关系。
一个复杂的类可能是由多个组件构成的。例如,汽车是由发动机、车身、底盘等组件构成的,而这些组件本身也是类,如发动机类、车身类或底盘类。类是有属性的,属性是类的数据特征,例如马是有颜色的,颜色是马这个类的属性,如果具体到作为对象的一匹马,则有的马是白色的,有的马是黑色的。不同的具体的马作为对象,可以有不同的属性值。
类也是有行为的。行为定义了类可以做什么事(具有什么功能),例如马可以奔跑,这是马的一种功能。
面向对象编程的设计思路体现了一种模块化建造、分而治之的思想,就像造汽车,工厂根据不同部件的关联程度将汽车分成几个模块,分发到不同的车间来建造。例如发动机车间专门造发动机,车身车间专门造车身,底盘车间专门造底盘,总装车间根据接口将它们组装起来,各车间只需专注自己的模块的建造,互不干扰。而且在研发新款车型时,可能只需要更改底盘,而老款的发动机仍可以使用。面向对象编程的优点归纳如下。
■ 分别用类来封装各自的数据和函数,代码相对独立,更容易修改和管理。
■ 让设计思路更清晰,编程更高效,代码更容易理解且不容易出错。
■ 可以直接使用现成的类,代码能更好地重用。
这里还是以2.2节的井字棋游戏为例进行说明。在面向对象编程的设计思路下,游戏本身是一个类,某个特定的游戏是一个对象,它是这个类的具体实例。游戏类包括一个重要的组件,也就是棋盘类。棋盘类包含两个属性,也就是棋盘大小和棋盘空间本身。棋盘类也包括若干函数,例如显示棋盘信息函数、判断胜负函数等。作为游戏类的组件,棋盘类还有一个属性,即当前需要落子的玩家。另外游戏类也包括若干函数,例如处理玩家输入的函数等。
我们可以根据游戏逻辑,将游戏程序用类图的形式进行重新设计。图2-4所示为设计好的游戏类Game和棋盘类Board的类图。类图中显示了各个类中包含的属性和函数。它可以让我们更清楚地检查类的设计,以及类之间的关系。
图2-4
下面我们根据面向对象编程的设计思路来修改原有的代码。首先定义棋盘类,也就是Board类。在初始化函数中定义两个属性,分别是棋盘大小属性size,以及棋子信息属性pieces。
class Board: def__init__(self,size): self.size=size self.pieces=['.'] * size * size
Board类有显示棋盘的函数show。
def show(self): print("\n") print("%s|%s|%s"%(self.pieces[0],self.pieces[1],self.pieces[2])) print("-+-+-") print("%s|%s|%s"%(self.pieces[3],self.pieces[4],self.pieces[5])) print("-+-+-") print("%s|%s|%s"%(self.pieces[6],self.pieces[7],self.pieces[8]))
Board类也有用于判断有没有空白位置可以落子的hasMovesLeft函数。
def hasMovesLeft(self): return'.'inself.pieces
Board类还需要有一个用于判断当前落子位置是否符合游戏规则的isMoveValid函数,以及用于落子后修改棋子信息的setMove函数。因为用户的输入是二维的坐标数据,所以需要建立一个辅助函数locToMove,负责将输入从二维的棋盘坐标转换为一维的列表索引。
def locToMove(self,loc): return int(loc[1]+loc[0]*self.size) def isMoveValid(self,loc): move=self.locToMove(loc) if self.pieces[move]=='.': return True else: return False def setMove(self,loc,player): move=self.locToMove(loc) self.pieces[move]=player
Board类当然也包括判断棋局胜负的hasWon函数。这里的实现逻辑和2.2节中代码显示的一样。
def hasWon(self,player): winningSet=[player in range(self.size)] row1=self.pieces[:3] row2=self.pieces[3:6] row3=self.pieces[6:] if winningSet in [row1,row2,row3]: return True col1=[self.pieces[0],self.pieces[3],self.pieces[6]] col2=[self.pieces[1],self.pieces[4],self.pieces[7]] col3=[self.pieces[2],self.pieces[5],self.pieces[8]] if winningSet in [col1,col2,col3]: return True diag1=[self.pieces[0],self.pieces[4],self.pieces[8]] diag2=[self.pieces[6],self.pieces[4],self.pieces[2]] if winningSet in [diag1,diag2]: return True return False
然后在Board类基础上构造游戏类,即Game类。Game类的初始化函数中包括当前玩家属性currentPlayer,以及前面定义好的Board类,将其实例化,把生成的棋盘对象作为Game类的一个组件。
class Game: def__init__(self,boardSize,startPlayer): self.currentPlayer=startPlayer self.board=Board(boardSize) print("井字棋游戏开始") print("规则:三子连成直线即胜利") print("X先手,O后手")
Game类中包括轮换玩家的getNextPlayer函数。
@staticmethod def getNextPlayer(currentPlayer): if currentPlayer=='X': return'O' else: return'X'
Game类中还包括处理玩家输入的getPlayerMove函数。
def getPlayerMove(self): while(True): userMove=input(f'\n玩家{self.currentPlayer}输入棋盘坐标(坐标取值0,1,2):X,Y?') userMoveLoc=[int(char) for char in userMove.split(',')] if self.board.isMoveValid(userMoveLoc): self.board.setMove(userMoveLoc,self.currentPlayer) break
然后将完整的游戏逻辑整合到play函数中。
def play(self): self.board.show() while self.board.hasMovesLeft(): self.getPlayerMove() self.board.show() if self.board.hasWon(self.currentPlayer): print('\n玩家'+self.currentPlayer+'胜利!') break self.currentPlayer=self.getNextPlayer(self.currentPlayer)
最终的main入口非常简洁。我们只需要将Game类实例化,生成game对象,再调用其play函数即可。在代码重构过程中,我们将原本零散的代码进行了划分,按类进行重构,使其逻辑层次更为清晰、更容易理解。完整的代码可以参见第2章的对应代码文件tic_human_class.py。
if__name__=='__main__': game=Game(boardSize=3,startPlayer='X') game.play()