面向对象编程应用
在之前的课程中,我们掌握了面向对象编程(OOP)的核心概念——类、对象、属性、方法,以及继承、封装、多态三大特性,并总结了“定义类→创建对象→给对象发消息”的三步走方法。但面向对象编程的难点不在于理解概念,而在于灵活应用,需要通过大量练习和阅读优质代码积累经验。
本节课将通过两个经典案例(扑克游戏、工资结算系统),深入剖析面向对象编程的实际用法,同时串联此前所学的Python知识,帮助大家打通“概念”与“应用”的壁垒。
例子1:扑克游戏(面向对象实战)
需求分析
设计一个简化版扑克游戏,规则如下:
类的抽象与设计
面向对象编程的核心是“抽象对象”,可通过需求中的“名词”提炼对象(类),“动词”提炼方法(行为),“名词属性”提炼类的属性:
枚举类:定义花色(提升代码可读性)
牌的花色仅有4种(黑桃、红心、草花、方块),属于“有限可选值”场景,适合用枚举类型表示。Python无专门枚举关键字,需继承enum模块的Enum类创建枚举。
from enum import EnumclassSuite(Enum):"""花色(枚举)""" SPADE, HEART, CLUB, DIAMOND = range(4)
通过上面的代码可以看出,定义枚举类型其实就是定义符号常量,如SPADE、HEART等。每个符号常量都有与之对应的值,这样表示黑桃就可以不用数字 0,而是用Suite.SPADE;同理,表示方块可以不用数字 3, 而是用Suite.DIAMOND。注意,使用符号常量肯定是优于使用字面常量的,因为能够读懂英文就能理解符号常量的含义,代码的可读性会提升很多。Python 中的枚举类型是可迭代类型,简单的说就是可以将枚举类型放到for-in循环中,依次取出每一个符号常量及其对应的值,如下所示。
for suite in Suite:print(f'{suite}: {suite.value}')
接下来我们可以定义牌类。
classCard:"""牌"""def__init__(self, suite, face):self.suite = suiteself.face = facedef__repr__(self): suites = '♠♥♣♦' faces = ['', 'A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']returnf'{suites[self.suite.value]}{faces[self.face]}'# 返回牌的花色和点数
可以通过下面的代码来测试下Card类。
card1 = Card(Suite.SPADE, 5)card2 = Card(Suite.HEART, 13)print(card1) # ♠5 print(card2) # ♥K
接下来我们定义扑克类。
import randomclassPoker:"""扑克"""def__init__(self):self.cards = [Card(suite, face) for suite in Suitefor face inrange(1, 14)] # 52张牌构成的列表self.current = 0# 记录发牌位置的属性defshuffle(self):"""洗牌"""self.current = 0 random.shuffle(self.cards) # 通过random模块的shuffle函数实现随机乱序defdeal(self):"""发牌""" card = self.cards[self.current]self.current += 1return card @propertydefhas_next(self):"""还有没有牌可以发"""returnself.current < len(self.cards)
可以通过下面的代码来测试下Poker类。
poker = Poker()print(poker.cards) # 洗牌前的牌poker.shuffle()print(poker.cards) # 洗牌后的牌
定义玩家类。
classPlayer:"""玩家"""def__init__(self, name):self.name = nameself.cards = [] # 玩家手上的牌defget_one(self, card):"""摸牌"""self.cards.append(card)defarrange(self):"""整理手上的牌"""self.cards.sort()
创建四个玩家并将牌发到玩家的手上。
poker = Poker()poker.shuffle()players = [Player('东邪'), Player('西毒'), Player('南帝'), Player('北丐')]# 将牌轮流发到每个玩家手上每人13张牌for _ inrange(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类代码如下所示。
classCard:"""牌"""def__init__(self, suite, face):self.suite = suiteself.face = facedef__repr__(self): suites = '♠♥♣♦' faces = ['', 'A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']returnf'{suites[self.suite.value]}{faces[self.face]}'def__lt__(self, other):ifself.suite == other.suite:returnself.face < other.face # 花色相同比较点数的大小returnself.suite.value < other.suite.value # 花色不同比较花色对应的值
说明: 大家可以尝试在上面代码的基础上写一个简单的扑克游戏,如 21 点游戏(Black Jack),游戏的规则可以自己在网上找一找。
例子2:工资结算系统(继承与多态实战)
需求分析
设计工资结算系统,支持三种员工类型的月薪计算:
通过对上述需求的分析,可以看出部门经理、程序员、销售员都是员工,有相同的属性和行为,那么我们可以先设计一个名为Employee的父类,再通过继承的方式从这个父类派生出部门经理、程序员和销售员三个子类。很显然,后续的代码不会创建Employee 类的对象,因为我们需要的是具体的员工对象,所以这个类可以设计成专门用于继承的抽象类。Python 语言中没有定义抽象类的关键字,但是可以通过abc模块中名为ABCMeta 的元类来定义抽象类。关于元类的概念此处不展开讲解,当然大家不用纠结,照做即可。
from abc import ABCMeta, abstractmethodclassEmployee(metaclass=ABCMeta):"""员工"""def__init__(self, name):self.name = name @abstractmethoddefget_salary(self):"""结算月薪"""pass
在上面的员工类中,有一个名为get_salary的方法用于结算月薪,但是由于还没有确定是哪一类员工,所以结算月薪虽然是员工的公共行为但这里却没有办法实现。对于暂时无法实现的方法,我们可以使用abstractmethod装饰器将其声明为抽象方法,所谓抽象方法就是只有声明没有实现的方法,声明这个方法是为了让子类去重写这个方法。接下来的代码展示了如何从员工类派生出部门经理、程序员、销售员这三个子类以及子类如何重写父类的抽象方法。
classManager(Employee):"""部门经理"""defget_salary(self):return15000.0classProgrammer(Employee):"""程序员"""def__init__(self, name, working_hour=0):super().__init__(name)self.working_hour = working_hourdefget_salary(self):return200 * self.working_hourclassSalesman(Employee):"""销售员"""def__init__(self, name, sales=0):super().__init__(name)self.sales = salesdefget_salary(self):return1800 + self.sales * 0.05
上面的Manager、Programmer、Salesman三个类都继承自Employee,三个类都分别重写了get_salary方法。重写就是子类对父类已有的方法重新做出实现。相信大家已经注意到了,三个子类中的get_salary各不相同,所以这个方法在程序运行时会产生多态行为,多态简单的说就是调用相同的方法,不同的子类对象做不同的事情。
我们通过下面的代码来完成这个工资结算系统,由于程序员和销售员需要分别录入本月的工作时间和销售额,所以在下面的代码中我们使用了 Python 内置的isinstance函数来判断员工对象的类型。我们之前讲过的type函数也能识别对象的类型,但是isinstance函数更加强大,因为它可以判断出一个对象是不是某个继承结构下的子类型,你可以简单的理解为type函数是对对象类型的精准匹配,而isinstance函数是对对象类型的模糊匹配。
emps = [Manager('刘备'), Programmer('诸葛亮'), Manager('曹操'), Programmer('荀彧'), Salesman('张辽')]for emp in emps:ifisinstance(emp, Programmer): emp.working_hour = int(input(f'请输入{emp.name}本月工作时间: '))elifisinstance(emp, Salesman): emp.sales = float(input(f'请输入{emp.name}本月销售额: '))print(f'{emp.name}本月工资为: ¥{emp.get_salary():.2f}元')
今日总结
面向对象编程的核心优势的是“符合人类思维习惯”,通过抽象、封装、继承、多态四大特性,让代码更具可读性、可维护性和可扩展性,但灵活运用需要长期积累:
抽象:从需求中提炼核心对象(类),明确属性与行为,忽略无关细节;
封装:将对象的属性和方法封装在类内部,对外提供统一接口,隐藏实现细节;
继承:提取类的共同特征,减少代码重复,构建类的层次结构;
多态:通过子类重写父类方法,实现“同一调用,不同行为”,提升代码灵活性。
建议大家在课后尝试扩展案例(如给扑克游戏添加“比大小”规则,给工资系统添加“员工考勤扣减”),同时阅读开源项目的优质代码,逐步提升面向对象编程的应用能力。