Python面向对象设计应用发牌游戏

中科治疗白癜风有疗效 http://www.bdfyy999.com/

面向对象程序设计(ObjectOrientedProgramming,OOP)的思想主要是针对大型软件的设计而提出的,它使软件的设计更加灵活,能够很好地支持代码复用和设计复用,并且使得代码具有更好的可读性和可扩展性。面向对象程序设计的一个关键性的理念是将数据以及对数据的操作封装在一起,组成一个相互依存、不可分割的整体,即对象。对相同类型的对象进行分类、抽象后,得出共同的特征就形成了类,面向对象程序设计的关键就是合理地定义和组织这些类以及类之间的关系。本期介绍面向对象程序设计中类和对象的定义,类的继承、派生与多态,最后应用扑克牌类设计发牌程序来帮助读者理解面向对象程序设计的理念。

1.游戏介绍

四名牌手打牌,电脑随机将52张牌(不含大、小王)发给四名牌手,并在屏幕上显示每位牌手的牌。本期采用扑克牌类设计扑克牌发牌程序。程序的运行效果如图1所示。

图1扑克牌发牌程序运行效果

2.Python面向对象设计

现实生活中的每一个相对独立的事物都可以看作一个对象,例如,一个人、一辆车、一台计算机等。对象是具有某些特性和功能的具体事物的抽象。每个对象都具有描述其特征的属性及附属于它的行为。例如,一辆车有颜色、车轮数、座椅数等属性,也有启动、行驶、停止等行为;一个人是由姓名、性别、年龄、身高、体重等特征描述,也有走路、说话、学习、开车等行为。

一台计算机由主机、显示器、键盘、鼠标等部件组成。当人们生产一台计算机时,并不是先要生产主机,再生产显示器,最后再生产键盘与鼠标,即不是顺序执行的。而是分别生产主机、显示器、键盘、鼠标等,最后把它们组装起来。这些部件通过事先设计好的接口连接,以便协调地工作。这就是面向对象程序设计的基本思路。

每个对象都有一个类,类是创建对象实例的模板,是对对象的抽象和概括,它包含对所创建对象的属性描述和行为特征的定义。例如,我们在马路上看到的汽车都是一个一个的汽车对象,它们通通归属于汽车类,那么,车身颜色就是该类的属性,开动是它的方法,保养或者报废就是它的事件。

Python采用了面向对象程序设计的思想,是真正的面向对象的高级动态编程语言,完全支持面向对象的基本功能,如封装、继承、多态以及对基类方法的覆盖或重写。但与其他面向对象程序设计语言不同的是,Python中对象的概念很宽泛,Python中的一切内容都可以称为对象。例如,字符串、列表、字典、元组等内置数据类型都具有与类相似的语法和用法。

2.1定义和使用类

2.1.1类定义

创建类时,用变量形式表示的对象属性称为数据成员或属性(成员变量),用函数形式表示的对象行为称为成员函数(成员方法),成员属性和成员方法统称为类的成员。

类定义的最简单形式如下:

在Person类中定义一个成员函数SayHello(self),用于输出字符串"Hello!"。同样地,Python使用缩进标识类的定义代码。

2.1.2对象定义

对象是类的实例。如果人类是一个类的话,那么某个具体的人就是一个对象。只有定义了具体的对象,才可通过“对象名。成员”的方式来访问其中的数据成员或成员方法。

Python创建对象的语法如下:

对象名=类名()

例如,下面的代码定义了一个类Person的对象p:

2.2构造函数__init__

类可以定义一个特殊的称为__init__()的方法(构造函数,以两个下画线“_”开头和结束)。一个类定义了__init__()方法以后,类实例化时就会自动为新生成的类实例调用__init__()方法。构造函数一般用于完成对象数据成员设置初值或进行其他必要的初始化工作。如果未定义构造函数,Python将提供一个默认的构造函数。

例如,定义一个复数类Complex,构造函数完成对象变量初始化工作。

运行结果如下:

2.析构函数

Python中类的析构函数是__del__,用来释放对象占用的资源,在Python收回对象空间之前自动执行。如果用户未定义析构函数,则Python会提供一个默认的析构函数进行必要的清理工作。

例如:

运行结果如下:

说明:在删除x对象变量之前,x是存在的,在内存中的标识为0x01F87C90。执行“delx”语句后,x对象变量就不存在了,系统自动调用析构函数,所以显示“Complex不存在了”。

2.4实例属性和类属性

属性(成员变量)有两种,一种是实例属性,另一种是类属性(类变量)。实例属性是在构造函数__init__(以两个下画线“_”开头和结束)中定义的,定义时以self作为前缀;类属性是在类中方法之外定义的属性。在主程序中(在类的外部),实例属性属于实例(对象)只能通过对象名访问;类属性属于类,可通过类名进行访问,也可以通过对象名进行访问,为类的所有实例共享。

定义含有实例属性(姓名name,年龄age)和类属性(人数num)的Person人员类。

num变量是一个类变量,它的值将在这个类的所有实例之间共享,用户可以在类内部或类外部使用Person.num访问num变量。

在类的成员函数(方法)中可以调用类的其他成员函数(方法),访问类属性、对象实例属性。

Python比较特殊的一点是,它可以动态地为类和对象增加成员,这一点是与很多面向对象程序设计语言不同的,也是Python动态类型特点的一个重要体现。

2.5私有成员与公有成员

Python并没有对私有成员提供严格的访问保护机制。在定义类的属性时,如果属性名以两个下画线“_”开头,则表示其是私有属性,否则是公有属性。私有属性在类的外部不能直接访问,需要通过调用对象的公有成员方法来访问,或者通过Python支持的特殊方式来访问。Python提供了访问私有属性的特殊方式,可用于程序的测试和调试,对成员方法也具有同样的性质。方法如下:

对象名._类名+私有成员

例如:访问Car类私有成员__weight

car1._Car__weight

私有属性是为了数据封装和保密而设置的属性,一般只能在类的成员方法(类的内部)中使用访问。虽然Python支持用户以一种特殊的方式从外部直接访问类的私有成员,但是并不推荐使用这种方法。公有属性是可以公开使用的,既可以在类的内部进行访问,也可以在外部程序中使用。

为Car类定义私有成员。

运行结果如下:

2.6方法

在类中进行定义的方法可以分为三大类:公有方法、私有方法和静态方法。其中,公有方法、私有方法都属于对象,私有方法的名字以两个下画线“_”开始,每个对象都有自己的公有方法和私有方法,在这两类方法中可以访问属于类和对象的成员;公有方法通过对象名直接调用,私有方法不能通过对象名直接调用,只能在属于对象的方法中通过“self”调用或在外部通过Python支持的特殊方式来调用。如果通过类名来调用属于对象的公有方法,则需要显式地为该方法的“self”参数传递一个对象名,用来明确指定访问哪个对象的数据成员。静态方法可以通过类名和对象名调用,但不能直接访问属于对象的成员,只能访问属于类的成员。

公有方法、私有方法、静态方法的定义和调用实例。

程序运行结果如下:

2.7类的继承

继承是为代码复用和设计复用而设计的,是面向对象程序设计的重要特性之一。当我们设计一个新类时,如果可以继承一个已有的、设计良好的类,然后进行二次开发,无疑会大幅减少开发的工作量。

在继承关系中,已有的、设计好的类称为父类或基类,新设计的类称为子类或派生类。派生类可以继承父类的公有成员,但是不能继承其私有成员。

类继承语法:

在Python中,继承的一些特点如下。

(1)在继承中,基类的构造函数(__init__()方法)不会被自动调用,它需要在其派生类的构造中专门调用。

(2)如果需要在派生类中调用基类的方法时,可通过“基类名.方法名()”的方式来实现,需要加上基类的类名前缀,且需要带上self参数变量,区别于在类中调用普通函数时并不需要带上self参数。也可以使用内置函数super()实现这一目的。

()Python总是首先查找对应类型的方法,如果不能在派生类中找到对应的方法,它才会到基类中逐个查找(先在本类中查找调用的方法,找不到才去基类中找)。

设计Person类,并根据Person派生Student类,分别创建Person类与Student类的对象。

运行结果如下:

方法重写必须出现在继承中。它是指当派生类继承了基类的方法之后,如果基类方法的功能不能满足需求,则需要对基类中的某些方法进行修改。可以在派生类重写基类的方法。

重写父类(基类)的方法。

程序运行结果如下:

当子类Dog和父类Animal都存在相同的run()方法时,子类的run()覆盖了父类的run()。在代码运行时,总是会调用子类的run()。这样,就获得了继承的另一个优点:多态。

2.8多态

要理解什么是多态,我们首先要对数据类型再做一点说明。当我们定义一个类的时候,实际上就定义了一种数据类型。这种定义的数据类型与Python自带的数据类型,如string、list、dict没有区别。

判断一个变量是否是某个类型,可以用isinstance()判断:

a、b、c对应着list、Animal、Dog这三种类型。

因为Dog是从Animal继承下来的,所以当我们创建了一个Dog的实例c时,可认为c的数据类型是Dog,也可认为C的数据类型是Animal。

因此,在继承关系中,如果一个实例的数据类型是某个子类,则它的数据类型也可以被看作是父类。但是,反过来就不行:

Dog可以看成Animal,但Animal不可以看成Dog。

要理解多态的好处,我们还需要再编写一个函数,这个函数接受一个Animal类型的变量:

当我们传入Animal的实例时,run_twice()就打印出:

当我们传入Dog的实例时,run_twice()就打印出:

当我们传入Cat的实例时,run_twice()就打印出:

现在,如果我们再定义一个Tortoise类型,也从Animal派生:

当我们调用run_twice()时,传入Tortoise的实例:

会发现新增一个Animal的子类,不必对run_twice()做任何修改。实际上,任何依赖Animal作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。

多态的好处就是,当我们需要传入Dog、Cat、Tortoise……时,我们只需要接收Animal类型就可以了,因为Dog、Cat、Tortoise……都是Animal类型。然后,按照Animal类型进行操作即可。由于Animal类型有run()方法,因此,传入的类型只要是Animal类或者子类,就会自动调用实际类型的run()方法,这就是多态的含义。

对于一个变量,我们只需要知道它是Animal类型,无须确切地知道它的子类型,就可以放心地调用run()方法。至于具体调用的run()方法是作用在Animal、Dog、Cat,还是Tortoise对象上,由运行时该对象的确切类型决定。这就是多态真正的优点:调用方只需调用即可,不用理会细节。而当我们新增一种Animal的子类时,只要确保run()方法编写正确,则不用管原来的代码是如何调用的。这就是著名的“开闭”原则。

对扩展开放:允许新增Animal子类。

对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。

.程序设计的步骤

.1设计类

发牌程序设计出三个类:Card类、Hand类和Poke类。

1.Card类

Card类代表一张牌,其中,FaceNum字段指的是牌面数字1~1,Suit字段指的是花色,"梅"为梅花,"方"为方块,"红"为红桃,"黑"为黑桃。

其中:

(1)Card构造函数根据参数初始化封装的成员变量,实现牌面大小和花色的初始化,以及是否显示牌面,默认True为显示牌正面;

(2)__str__()方法用来输出牌面大小和花色;

()pic_order()方法获取牌的顺序号,牌面按梅花1~1,方块14~26,红桃27~9,黑桃40~52顺序编号(未洗牌之前),也就是说,梅花2顺序号为2,方块A顺序号为14,方块K顺序号为26(这个方法为图形化显示牌面预留的方法);

(4)flip()是翻牌方法,改变牌面是否显示的属性值。

2.Hand类

Hand类代表手牌(一个玩家手里拿的牌),可以认为是一位牌手手里的牌,其中,cards列表变量存储牌手手中的牌。可以增加牌、清空手里的牌、把一张牌给别的牌手等操作。

.Poke类

Poke类代表一副牌,我们可以将一副牌看作是有52张牌的牌手,所以继承Hand类。由于其中cards列表变量要存储52张牌,而且要进行发牌、洗牌操作,所以增加如下的方法。

(1)populate(self)生成存储了52张牌的一副牌,当然这些牌是按梅花1~1,方块14~26,红桃27~9,黑桃40~52的顺序(未洗牌之前)存储在cards列表变量。

(2)shuffle(self)洗牌,使用Python的random模块shuffle()方法打乱牌的存储顺序即可。

()deal(self,hands,per_hand=1)可完成发牌动作,发给四个牌手,每人默认1张牌。当然,若令per_hand=10,则给每个牌手发10张牌,只不过最后仍有牌没发完。

.2主程序

主程序比较简单,因为有四个牌手,所以生成players列表存储初始化的四位牌手。生成一副牌的对象实例poke1,调用populate()方法生成有52张牌的一副牌,调用shuffle()方法洗牌打乱顺序,调用deal(players,1)方法分别给每位玩家发1张牌,最后显示四位牌手所有的牌。




转载请注明:http://www.aierlanlan.com/cyrz/768.html