2.3 面向对象编程

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()