举例一:扑克游戏     

       简单起见,我们的扑克只有52张牌(没有大小王),游戏需要将 52 张牌发到 4 个玩家的手上,每个玩家手上有 13 张牌,按照黑桃、红心、草花、方块的顺序和点数从小到大排列,暂时不实现其他的功能。

       使用面向对象编程方法,首先需要从问题的需求中找到对象并抽象出对应的类,此外还要找到对象的属性和行为。     

分析:扑克游戏中至少应该有三类对象,分别是牌、扑克和玩家。这三类并不是孤立的。 

类和类之间的关系可以粗略的分为 is-a关系(继承)、**has-a关系(关联)**和 use-a关系(依赖)。很显然扑克和牌是 has-a 关系,因为一副扑克有(has-a)52 张牌;玩家和牌之间不仅有关联关系还有依赖关系,因为玩家手上有(has-a)牌而且玩家使用了(use-a)牌。

步骤一:“牌”类

下面我们对于这个游戏分几步开始写,首先我们先建立“牌”这个类。 

from enum import Enum

class Suite(Enum):
    """花色(枚举)"""
    SPADE, HEART, CLUB, DIAMOND = range(4)

for suite in Suite:
    print(f'{suite}: {suite.value}')   # 用0,1,2,3对应这个四个花色

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]}'  # 返回牌的花色和点数

card1 = Card(Suite.SPADE, 5)
card2 = Card(Suite.HEART, 12)
print(card1)  # ♠5
print(card2)  # ♥K

这段代码就相当于我们把牌已经买回来,一副新牌。

1、在 Python 中,Suite 本身并不是一个内置的关键字或预定义类。从你之前的代码示例来看,Suite 是一个自定义的 枚举类(Enum),用于表示扑克牌的四种花色。

2、__repr__ 是 Python 的特殊方法,用于返回对象的 “官方” 字符串表示形式。定义__repr__函数,其作用是将suite.value对应的值和suites中的花色一一对应起来。

步骤二:“扑克”类

接下来,我们定了“扑克”,首先,扑克和牌是相关的,所以扑克是牌的继承。同时,由于在“牌”类中并没有定义牌的位置,我们需要给“扑克”类添加“属性”,以及“洗牌”,“发牌”的行为。下面代码如下,

import random


class Poker:
    """扑克"""

    def __init__(self):
        self.cards = [Card(suite, face) 
                      for suite in Suite
                      for face in range(1, 14)]  # 52张牌构成的列表
        self.current = 0  # 记录发牌位置的属性

    def shuffle(self):
        """洗牌"""
        self.current = 0
        random.shuffle(self.cards)  # 通过random模块的shuffle函数实现随机乱序

    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 = Poker()
print(poker.cards)  # 洗牌前的牌
poker.shuffle()
print(poker.cards)  # 洗牌后的牌

在 Poker 类中,self.current 是一个发牌指针,用于记录当前发牌的位置。它的作用是追踪已经发出了多少张牌,从而控制下一张要发的牌是哪一张。

每次调用 deal() 时,self.current 会指示当前要发的牌,并在发牌后加 1,指向下一张牌:

  • 第一次调用 deal()self.current 为 0,返回第一张牌,然后 self.current 变为 1
  • 第二次调用 deal()self.current 为 1,返回第二张牌,然后 self.current 变为 2
  • 依此类推,直到发完所有 52 张牌。

步骤三:“玩家”类

定义了一个 Player 类,用于表示扑克牌游戏中的玩家。这个类包含玩家的基本属性(如姓名)和行为(摸牌、整理牌)。

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

步骤四:开始完游戏

poker = Poker()
poker.shuffle()
players = [Player('东邪'), Player('西毒'), Player('南帝'), Player('北丐')]
# 将牌轮流发到每个玩家手上每人13张牌
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()那里出现异常,因为Playerarrange方法使用了列表的sort对玩家手上的牌进行排序,排序需要比较两个Card对象的大小,而 运算符又不能直接作用于Card类型。也就是说,Python中只定义了对数、以及字符串进行比较,对于代码中花色的比较Python中并没有设置。

  • 但对于自定义类(如 Card),对象的 “大小” 是抽象的,没有默认规则:
    • 两张牌 card1 和 card2,Python 不知道是按 “花色” 比较,还是按 “点数” 比较,也不知道 “黑桃” 和 “红桃” 谁大谁小。
    • 因此,当你调用 self.cards.sort() 时,Python 会报错 TypeError: '<' not supported between instances of 'Card' and 'Card',因为它找不到比较 Card 对象的规则。

因此对于“牌”类应该修改为

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   # 花色不同比较花色对应的值

 例二:工资结算系统

要求:

某公司有三种类型的员工,分别是部门经理、程序员和销售员。需要设计一个工资结算系统,根据提供的员工信息来计算员工的月薪。其中,部门经理的月薪是固定 15000 元;程序员按工作时间(以小时为单位)支付月薪,每小时 200 元;销售员的月薪由 1800 元底薪加上销售额 5% 的提成两部分构成。

这个游戏也可分步骤进行。

步骤一:建立牛马总类

       通过对上述需求的分析,可以看出部门经理、程序员、销售员都是员工,有相同的属性和行为,那么我们可以先设计一个名为Employee的父类,再通过继承的方式从这个父类派生出部门经理、程序员和销售员三个子类。

为什么在抽象类中要用 @abstractmethod 装饰器?

强制子类遵守规则
抽象类的 calculate_salary 方法只是 “声明” 了 “员工需要算工资”,但没说具体怎么算(因为不同员工算法不同)。
用 @abstractmethod 装饰后,就像立下规矩:“所有继承我的子类,必须自己实现这个方法,不然不准用!”
比如,如果程序员类忘了写 calculate_salary,代码一运行就会报错,提醒你 “漏了必做的事”。

from abc import ABCMeta, abstractmethod


class Employee(metaclass=ABCMeta):
    """员工"""

    def __init__(self, name):
        self.name = name

    @abstractmethod
    def get_salary(self):
        """结算月薪"""
        pass

步骤二:具体的牛马类

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

步骤三:工资结算系统

 我们通过下面的代码来完成这个工资结算系统,由于程序员和销售员需要分别录入本月的工作时间和销售额,所以在下面的代码中我们使用了 Python 内置的isinstance函数来判断员工对象的类型。我们之前讲过的type函数也能识别对象的类型,但是isinstance函数更加强大,因为它可以判断出一个对象是不是某个继承结构下的子类型,你可以简单的理解为type函数是对对象类型的精准匹配,而isinstance函数是对对象类型的模糊匹配。

emps = [Manager('刘备'), Programmer('诸葛亮'), Manager('曹操'), Programmer('荀彧'), Salesman('张辽')]
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}元')

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐