Python-Core-50-Courses/第018课:面向对象编程应用.md

242 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## 第018课面向对象编程应用
面向对象编程对初学者来说不难理解但很难应用,虽然我们为大家总结过面向对象的三步走方法(定义类、创建对象、给对象发消息),但是说起来容易做起来难。**大量的编程练习**和**阅读优质的代码**可能是这个阶段最能够帮助到大家的两件事情。接下来我们还是通过经典的案例来剖析面向对象编程的知识同时也通过这些案例为大家讲解如何运用之前学过的Python知识。
### 经典案例
#### 案例1扑克游戏。
> **说明**简单起见我们的扑克只有52张牌没有大小王游戏需要将52张牌发到4个玩家的手上每个玩家手上有13张牌按照黑桃、红心、草花、方块的顺序和点数从小到大排列暂时不实现其他的功能。
使用面向对象编程方法,首先需要从问题的需求中找到对象并抽象出对应的类,此外还要找到对象的属性和行为。当然,这件事情并不是特别困难,我们可以从需求的描述中找出名词和动词,名词通常就是对象或者是对象的属性,而动词通常是对象的行为。扑克游戏中至少应该有三类对象,分别是牌、扑克和玩家,牌、扑克、玩家三个类也并不是孤立的。类和类之间的关系可以粗略的分为**is-a关系继承**、**has-a关系关联**和**use-a关系依赖**。很显然扑克和牌是has-a关系因为一副扑克有has-a52张牌玩家和牌之间不仅有关联关系还有依赖关系因为玩家手上有has-a牌而且玩家使用了use-a牌。
牌的属性显而易见有花色和点数。我们可以用0到3的四个数字来代表四种不同的花色但是这样的代码可读性会非常糟糕因为我们并不知道黑桃、红心、草花、方块跟0到3的数字的对应关系。如果一个变量的取值只有有限多个选项我们可以使用枚举。与C、Java等语言不同的是Python中没有声明枚举类型的关键字但是可以通过继承`enum`模块的`Enum`类来创建枚举类型,代码如下所示。
```Python
from enum import Enum
class Suite(Enum):
"""花色(枚举)"""
SPADE, HEART, CLUB, DIAMOND = range(4)
```
通过上面的代码可以看出,定义枚举类型其实就是定义符号常量,如`SPADE`、`HEART`等。每个符号常量都有与之对应的值,这样表示黑桃就可以不用数字`0`,而是用`Suite.SPADE`;同理,表示方块可以不用数字`3` 而是用`Suite.DIAMOND`。注意使用符号常量肯定是优于使用字面常量的因为能够读懂英文就能理解符号常量的含义代码的可读性会提升很多。Python中的枚举类型是可迭代类型简单的说就是可以将枚举类型放到`for-in`循环中,依次取出每一个符号常量及其对应的值,如下所示。
```Python
for suite in Suite:
print(f'{suite}: {suite.value}')
```
接下来我们可以定义牌类。
```Python
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`类。
```Python
card1 = Card(Suite.SPADE, 5)
card2 = Card(Suite.HEART, 13)
print(card1, card2) # ♠5 ♥K
```
接下来我们定义扑克类。
```Python
import random
class Poker:
"""扑克"""
def __init__(self):
# 通过列表的生成式语法创建一个装52张牌的列表
self.cards = [Card(suite, face) for suite in Suite
for face in range(1, 14)]
# current属性表示发牌的位置
self.current = 0
def shuffle(self):
"""洗牌"""
self.current = 0
# 通过random模块的shuffle函数实现列表的随机乱序
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`类。
```Python
poker = Poker()
poker.shuffle()
print(poker.cards)
```
定义玩家类。
```Python
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()
```
创建四个玩家并将牌发到玩家的手上。
```Python
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`类代码如下所示。
```Python
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` 的元类来定义抽象类。关于元类的知识,后面的课程中会有专门的讲解,这里不用太纠结这个概念,记住用法即可。
```Python
from abc import ABCMeta, abstractmethod
class Employee(metaclass=ABCMeta):
"""员工"""
def __init__(self, name):
self.name = name
@abstractmethod
def get_salary(self):
"""结算月薪"""
pass
```
在上面的员工类中,有一个名为`get_salary`的方法用于结算月薪,但是由于还没有确定是哪一类员工,所以结算月薪虽然是员工的公共行为但这里却没有办法实现。对于暂时无法实现的方法,我们可以使用`abstractmethod`装饰器将其声明为抽象方法,所谓**抽象方法就是只有声明没有实现的方法****声明这个方法是为了让子类去重写这个方法**。接下来的代码展示了如何从员工类派生出部门经理、程序员、销售员这三个子类以及子类如何重写父类的抽象方法。
```Python
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`函数是对对象类型的模糊匹配。
```Python
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():.2f}元')
```
### 简单的总结
面向对象的编程思想非常的好,也符合人类的正常思维习惯,但是要想灵活运用面向对象编程中的抽象、封装、继承、多态需要长时间的积累和沉淀,这件事情并非一夕之功,也无法一蹴而就。
> **温馨提示**:学习中如果遇到困难,可以加**QQ交流群**询问。
>
> 付费群:**789050736**,群一直保留,供大家学习交流讨论问题。
>
> 免费群:**151669801**,仅供入门新手提问,定期清理群成员。