面向对象编程对初学者来说不难理解但很难应用,虽然我们为大家总结过面向对象的三步走方法(定义类、创建对象、给对象发消息),但是说起来容易做起来难。**大量的编程练习**和**阅读优质的代码**可能是这个阶段最能够帮助到大家的两件事情。接下来我们还是通过经典的案例来剖析面向对象编程的知识,同时也通过这些案例为大家讲解如何运用之前学过的Python知识。
一、经典案例 案例1:扑克游戏。
说明 :简单起见,我们的扑克只有52张牌(没有大小王),游戏需要将52张牌发到4个玩家的手上,每个玩家手上有13张牌,按照黑桃、红心、草花、方块的顺序和点数从小到大排列,暂时不实现其他的功能。
使用面向对象编程方法,首先需要从问题的需求中找到对象并抽象出对应的类,此外还要找到对象的属性和行为。当然,这件事情并不是特别困难,我们可以从需求的描述中找出名词和动词,名词通常就是对象或者是对象的属性,而动词通常是对象的行为。扑克游戏中至少应该有三类对象,分别是牌、扑克和玩家,牌、扑克、玩家三个类也并不是孤立的。类和类之间的关系可以粗略的分为is-a关系(继承) 、has-a关系(关联) 和use-a关系(依赖) 。很显然扑克和牌是has-a关系,因为一副扑克有(has-a)52张牌;玩家和牌之间不仅有关联关系还有依赖关系,因为玩家手上有(has-a)牌而且玩家使用了(use-a)牌。
牌的属性显而易见,有花色和点数。我们可以用0到3的四个数字来代表四种不同的花色,但是这样的代码可读性会非常糟糕,因为我们并不知道黑桃、红心、草花、方块跟0到3的数字的对应关系。如果一个变量的取值只有有限多个选项,我们可以使用枚举。与C、Java等语言不同的是,Python中没有声明枚举类型的关键字,但是可以通过继承enum
模块的Enum
类来创建枚举类型,代码如下所示。
1 2 3 4 5 6 from enum import Enumclass Suite (Enum) : """花色(枚举)""" SPADE, HEART, CLUB, DIAMOND = range(4 )
通过上面的代码可以看出,定义枚举类型其实就是定义符号常量,如SPADE
、HEART
等。每个符号常量都有与之对应的值,这样表示黑桃就可以不用数字0
,而是用Suite.SPADE
;同理,表示方块可以不用数字3
, 而是用Suite.DIAMOND
。注意,使用符号常量肯定是优于使用字面常量的,因为能够读懂英文就能理解符号常量的含义,代码的可读性会提升很多。Python中的枚举类型是可迭代类型,简单的说就是可以将枚举类型放到for-in
循环中,依次取出每一个符号常量及其对应的值,如下所示。
1 2 for suite in Suite: print(f'{suite} : {suite.value} ' )
接下来我们可以定义牌类。
1 2 3 4 5 6 7 8 9 10 11 12 class Card : """牌""" def __init__ (self, suite, face) : self.suite = suite self.face = face def __repr__ (self) : suites = '♠♥♣♦' faces = ['' , 'A' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , '10' , 'J' , 'Q' , 'K' ] return f'{suites[self.suite.value]} {faces[self.face]} '
可以通过下面的代码来测试下Card
类。
1 2 3 card1 = Card(Suite.SPADE, 5 ) card2 = Card(Suite.HEART, 13 ) print(card1, card2)
接下来我们定义扑克类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import randomclass Poker : """扑克""" def __init__ (self) : self.cards = [Card(suite, face) for suite in Suite for face in range(1 , 14 )] self.current = 0 def shuffle (self) : """洗牌""" self.current = 0 random.shuffle(self.cards) def deal (self) : """发牌""" card = self.cards[self.current] self.current += 1 return card @property def has_next (self) : """还有没有牌可以发""" return self.current < len(self.cards)
可以通过下面的代码来测试下Poker
类。
1 2 3 poker = Poker() poker.shuffle() print(poker.cards)
定义玩家类。
1 2 3 4 5 6 7 8 9 10 11 12 13 class Player : """玩家""" def __init__ (self, name) : self.name = name self.cards = [] def get_one (self, card) : """摸牌""" self.cards.append(card) def arrange (self) : self.cards.sort()
创建四个玩家并将牌发到玩家的手上。
1 2 3 4 5 6 7 8 9 10 poker = Poker() poker.shuffle() players = [Player('东邪' ), Player('西毒' ), Player('南帝' ), Player('北丐' )] for _ in range(13 ): for player in players: player.get_one(poker.deal()) for player in players: player.arrange() print(f'{player.name} : ' , end='' ) print(player.cards)
执行上面的代码会在player.arrange()
那里出现异常,因为Player
的arrange
方法使用了列表的sort
对玩家手上的牌进行排序,排序需要比较两个Card
对象的大小,而<
运算符又不能直接作用于Card
类型,所以就出现了TypeError
异常,异常消息为:'<' not supported between instances of 'Card' and 'Card'
。
为了解决这个问题,我们可以对Card
类的代码稍作修改,使得两个Card
对象可以直接用<
进行大小的比较。这里用到技术叫运算符重载 ,Python中要实现对<
运算符的重载,需要在类中添加一个名为__lt__
的魔术方法。很显然,魔术方法__lt__
中的lt
是英文单词“less than”的缩写,以此类推,魔术方法__gt__
对应>
运算符,魔术方法__le__
对应<=
运算符,__ge__
对应>=
运算符,__eq__
对应==
运算符,__ne__
对应!=
运算符。
修改后的Card
类代码如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Card : """牌""" def __init__ (self, suite, face) : self.suite = suite self.face = face def __repr__ (self) : suites = '♠♥♣♦' faces = ['' , 'A' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , '10' , 'J' , 'Q' , 'K' ] return f'{suites[self.suite.value]} {faces[self.face]} ' def __lt__ (self, other) : if self.suite == other.suite: return self.face < other.face return self.suite.value < other.suite.value
说明: 大家可以尝试在上面代码的基础上写一个简单的扑克游戏,如21点游戏(Black Jack),游戏的规则可以自己在网上找一找。
案例2:工资结算系统。
要求 :某公司有三种类型的员工,分别是部门经理、程序员和销售员。需要设计一个工资结算系统,根据提供的员工信息来计算员工的月薪。其中,部门经理的月薪是固定15000元;程序员按工作时间(以小时为单位)支付月薪,每小时200元;销售员的月薪由1800元底薪加上销售额5%的提成两部分构成。
通过对上述需求的分析,可以看出部门经理、程序员、销售员都是员工,有相同的属性和行为,那么我们可以先设计一个名为Employee
的父类,再通过继承的方式从这个父类派生出部门经理、程序员和销售员三个子类。很显然,后续的代码不会创建Employee
类的对象,因为我们需要的是具体的员工对象,所以这个类可以设计成专门用于继承的抽象类。Python中没有定义抽象类的关键字,但是可以通过abc
模块中名为ABCMeta
的元类来定义抽象类。关于元类的知识,后面的课程中会有专门的讲解,这里不用太纠结这个概念,记住用法即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 from abc import ABCMeta, abstractmethodclass Employee (metaclass=ABCMeta) : """员工""" def __init__ (self, name) : self.name = name @abstractmethod def get_salary (self) : """结算月薪""" pass
在上面的员工类中,有一个名为get_salary
的方法用于结算月薪,但是由于还没有确定是哪一类员工,所以结算月薪虽然是员工的公共行为但这里却没有办法实现。对于暂时无法实现的方法,我们可以使用abstractmethod
装饰器将其声明为抽象方法,所谓抽象方法就是只有声明没有实现的方法 ,声明这个方法是为了让子类去重写这个方法 。接下来的代码展示了如何从员工类派生出部门经理、程序员、销售员这三个子类以及子类如何重写父类的抽象方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Manager (Employee) : """部门经理""" def get_salary (self) : return 15000.0 class Programmer (Employee) : """程序员""" def __init__ (self, name, working_hour=0 ) : super().__init__(name) self.working_hour = working_hour def get_salary (self) : return 200 * self.working_hour class Salesman (Employee) : """销售员""" def __init__ (self, name, sales=0 ) : super().__init__(name) self.sales = sales def get_salary (self) : return 1800 + self.sales * 0.05
上面的Manager
、Programmer
、Salesman
三个类都继承自Employee
,三个类都分别重写了get_salary
方法。重写就是子类对父类已有的方法重新做出实现 。相信大家已经注意到了,三个子类中的get_salary
各不相同,所以这个方法在程序运行时会产生多态行为 ,多态简单的说就是调用相同的方法 ,不同的子类对象做不同的事情 。
我们通过下面的代码来完成这个工资结算系统,由于程序员和销售员需要分别录入本月的工作时间和销售额,所以在下面的代码中我们使用了Python内置的isinstance
函数来判断员工对象的类型。我们之前讲过的type
函数也能识别对象的类型,但是isinstance
函数更加强大,因为它可以判断出一个对象是不是某个继承结构下的子类型,你可以简答的理解为type
函数是对对象类型的精准匹配,而isinstance
函数是对对象类型的模糊匹配。
1 2 3 4 5 6 7 8 9 10 emps = [ Manager('刘备' ), Programmer('诸葛亮' ), Manager('曹操' ), Programmer('荀彧' ), Salesman('吕布' ), Programmer('张辽' ), ] for emp in emps: if isinstance(emp, Programmer): emp.working_hour = int(input(f'请输入{emp.name} 本月工作时间: ' )) elif isinstance(emp, Salesman): emp.sales = float(input(f'请输入{emp.name} 本月销售额: ' )) print(f'{emp.name} 本月工资为: ¥{emp.get_salary():.2 f} 元' )
二、最佳练习 练习1:奥特曼打小怪兽 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 from abc import ABCMeta, abstractmethodfrom random import randint, randrangeclass Fighter (object, metaclass=ABCMeta) : """战斗者""" __slots__ = ('_name' , '_hp' ) def __init__ (self, name, hp) : """初始化方法 :param name: 名字 :param hp: 生命值 """ self._name = name self._hp = hp @property def name (self) : return self._name @property def hp (self) : return self._hp @hp.setter def hp (self, hp) : self._hp = hp if hp >= 0 else 0 @property def alive (self) : return self._hp > 0 @abstractmethod def attack (self, other) : """攻击 :param other: 被攻击的对象 """ pass class Ultraman (Fighter) : """奥特曼""" __slots__ = ('_name' , '_hp' , '_mp' ) def __init__ (self, name, hp, mp) : """初始化方法 :param name: 名字 :param hp: 生命值 :param mp: 魔法值 """ super().__init__(name, hp) self._mp = mp def attack (self, other) : other.hp -= randint(15 , 25 ) def huge_attack (self, other) : """究极必杀技(打掉对方至少50点或四分之三的血) :param other: 被攻击的对象 :return: 使用成功返回True否则返回False """ if self._mp >= 50 : self._mp -= 50 injury = other.hp * 3 // 4 injury = injury if injury >= 50 else 50 other.hp -= injury return True else : self.attack(other) return False def magic_attack (self, others) : """魔法攻击 :param others: 被攻击的群体 :return: 使用魔法成功返回True否则返回False """ if self._mp >= 20 : self._mp -= 20 for temp in others: if temp.alive: temp.hp -= randint(10 , 15 ) return True else : return False def resume (self) : """恢复魔法值""" incr_point = randint(1 , 10 ) self._mp += incr_point return incr_point def __str__ (self) : return '~~~%s奥特曼~~~\n' % self._name + \ '生命值: %d\n' % self._hp + \ '魔法值: %d\n' % self._mp class Monster (Fighter) : """小怪兽""" __slots__ = ('_name' , '_hp' ) def attack (self, other) : other.hp -= randint(10 , 20 ) def __str__ (self) : return '~~~%s小怪兽~~~\n' % self._name + \ '生命值: %d\n' % self._hp def is_any_alive (monsters) : """判断有没有小怪兽是活着的""" for monster in monsters: if monster.alive > 0 : return True return False def select_alive_one (monsters) : """选中一只活着的小怪兽""" monsters_len = len(monsters) while True : index = randrange(monsters_len) monster = monsters[index] if monster.alive > 0 : return monster def display_info (ultraman, monsters) : """显示奥特曼和小怪兽的信息""" print(ultraman) for monster in monsters: print(monster, end='' ) def main () : u = Ultraman('骆昊' , 1000 , 120 ) m1 = Monster('狄仁杰' , 250 ) m2 = Monster('白元芳' , 500 ) m3 = Monster('王大锤' , 750 ) ms = [m1, m2, m3] fight_round = 1 while u.alive and is_any_alive(ms): print('========第%02d回合========' % fight_round) m = select_alive_one(ms) skill = randint(1 , 10 ) if skill <= 6 : print('%s使用普通攻击打了%s.' % (u.name, m.name)) u.attack(m) print('%s的魔法值恢复了%d点.' % (u.name, u.resume())) elif skill <= 9 : if u.magic_attack(ms): print('%s使用了魔法攻击.' % u.name) else : print('%s使用魔法失败.' % u.name) else : if u.huge_attack(m): print('%s使用究极必杀技虐了%s.' % (u.name, m.name)) else : print('%s使用普通攻击打了%s.' % (u.name, m.name)) print('%s的魔法值恢复了%d点.' % (u.name, u.resume())) if m.alive > 0 : print('%s回击了%s.' % (m.name, u.name)) m.attack(u) display_info(u, ms) fight_round += 1 print('\n========战斗结束!========\n' ) if u.alive > 0 : print('%s奥特曼胜利!' % u.name) else : print('小怪兽胜利!' ) if __name__ == '__main__' : main()
练习2:图书管理系统 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 """ DateTime : 2020/12/06 9:43 Author : ZhangYafei Description: 图书管理系统 1.查询图书 2.增加图书 3.借阅图书 4.归还图书 5.退出系统 """ """ c 谭浩强 未借出 ISO9001 python guido 未借出 NFS8102 java westos 未借出 PKI7844 """ class Book (object) : def __init__ (self,name,author,status,bookindex) : self.name = name self.author = author self.status = status self.bookindex = bookindex def __str__ (self) : if self.status == 1 : stats = '未借出' elif self.status == 0 : stats = '已借出' else : stats = '未知状态' return '书名:《%s》 作者: %s 状态: <%s> 位置: %s' \ %(self.name,self.author,stats,self.bookindex) class BooKManager (object) : books = [] def Start (self) : self.books.append(Book('c' ,'谭浩强' ,1 ,'ISO9001' )) self.books.append(Book('python' ,'guido' ,1 ,'NFS8102' )) self.books.append(Book('java' ,'westos' ,1 ,'PKI7844' )) def Menu (self) : self.Start() while True : print("""图书管理系统\n\t1.查询图书\n\t2.增加图书\n\t3.借阅图书\n\t4.归还图书\n\t5.退出系统""" ) choice = input('输入您的选择: ' ) if choice == '1' : self.ShowAllBook() elif choice == '2' : self.AddBook() elif choice == '3' : self.BorrowBook() elif choice == '4' : self.ReturnBook() elif choice == '5' : print('欢迎下次使用...' ) exit() else : print('请输入正确选择!' ) continue def CheckBook (self,name) : for book in self.books: if book.name == name: return book else : return None def ShowAllBook (self) : for book in self.books: print(book) def AddBook (self) : name = input('图书名称: ' ) self.books.append(Book(name,input('作者:' ),1 ,input('存储位置:' ))) print('图书《%s》增加成功' %name) def BorrowBook (self) : name = input('借阅图书名称: ' ) ret = self.CheckBook(name) print(ret) if ret != None : if ret.status == 0 : print('书籍《%s》已经借出' %name) else : ret.status = 0 print('书籍《%s》借阅成功' %name) else : print('书籍《%s》不存在' %name) def ReturnBook (self) : name = input('归还图书名称: ' ) ret = self.CheckBook(name) if ret != None : if ret.status == 0 : ret.status = 1 print('书籍《%s》归还成功' %name) print(ret) else : print('书籍《%s》未借出' %name) else : print('书籍《%s》不存在' %name) manage = BooKManager() manage.Menu()
三、简单的总结 面向对象的编程思想非常的好,也符合人类的正常思维习惯,但是要想灵活运用面向对象编程中的抽象、封装、继承、多态需要长时间的积累和沉淀,这件事情并非一夕之功,也无法一蹴而就。