使用 OOP 构建 Python 乘法表应用程序

使用面向对象编程在 Python 中构建乘法表应用

本文将引导你使用 Python 中面向对象编程 (OOP) 的强大功能来开发一个乘法表应用程序。你将深入实践 OOP 的核心概念,并学习如何在实际应用中运用它们。

Python 是一种支持多种编程范式的语言,这意味着作为开发者,我们可以根据不同的场景和问题选择最合适的方案。在面向对象编程方面,它是过去几十年中最常用的构建可扩展应用程序的方法之一。

面向对象的基础

让我们快速回顾一下 Python 中最重要的 OOP 概念:类。

类是定义对象结构和行为的蓝图。这个蓝图允许我们创建实例,这些实例是根据类结构创建的独立对象。

下面是一个简单的书籍类的定义,它具有标题和颜色属性:

class Book:
    def __init__(self, title, color):
        self.title = title
        self.color = color

要创建 Book 类的实例,我们需要调用类并传递参数。

# 创建 Book 类的实例
blue_book = Book("蓝色的小孩", "蓝色")
green_book = Book("青蛙的故事", "绿色")

以下是对当前计划的一个直观表示:

有趣的是,当我们检查 blue_book 和 green_book 实例的类型时,会得到“Book”。

# 打印书籍的类型

print(type(blue_book))
# <class '__main__.Book'>
print(type(green_book))
# <class '__main__.Book'>

理解了这些基础知识后,我们就可以开始构建项目了😃。

项目概述

据统计,作为开发人员,我们大部分时间并非花在编写代码上。根据New Stack的报道,我们大约只有三分之一的时间用于编写或重构代码。

其余三分之二的时间,我们都在阅读别人的代码和分析正在解决的问题。

因此,对于这个项目,我将先提出一个问题,然后分析如何基于它创建我们的应用程序。我们将完整地经历从思考解决方案到用代码实现它的过程。

一位小学老师希望有一个游戏来测试 8 到 10 岁学生的乘法技能。

该游戏需要有生命值和分数系统。学生从 3 条生命开始,并需要达到一定分数才能获胜。如果学生用尽所有生命,程序必须显示“失败”信息。

游戏需要有两种模式:随机乘法和乘法表。

第一种模式应该给学生一个 1 到 10 之间的随机乘法,如果回答正确则获得一分。如果回答错误,学生失去一条生命,游戏继续。只有当学生达到 5 分时才算获胜。

第二种模式必须显示 1 到 10 的乘法表,学生需要输入每个乘法的结果。如果学生失败 3 次则输掉游戏,但如果成功完成两张乘法表,则游戏结束。

我知道要求可能有点多,但我保证我们将在本文中解决它们😁。

分而治之的策略

编程中最重要的技能是解决问题的能力。在开始编写代码之前,我们需要有一个清晰的计划。

我建议将大问题分解为更容易管理的小问题,这样可以更简单有效地解决问题。

例如,如果你需要开发一个游戏,首先把它分解成最重要的部分。这些子问题更容易解决。

之后,你就可以清楚地了解如何执行所有内容并将其整合到代码中。

因此,让我们绘制一个游戏流程图。

此图展示了我们应用程序中对象之间的关系。如你所见,两个主要对象是随机乘法和乘法表。它们之间唯一共享的属性是分数和生命值。

牢记这些信息,让我们开始编写代码。

创建父类 Game

当我们使用面向对象编程时,我们寻求避免代码重复的最简洁方法。这就是所谓的 “DRY” (不要重复自己)原则。

注意:这个目标与减少代码行数无关(代码质量不能通过代码行数衡量),而是要抽象出最常用的逻辑。

根据之前的想法,我们应用程序的父类必须建立其他两个类的结构和预期的行为。

让我们来看看它是如何实现的。

class BaseGame:

    # 消息居中显示的长度
    message_lenght = 60
    
    description = ""    
        
    def __init__(self, points_to_win, n_lives=3):
        """基础游戏类

        Args:
            points_to_win (int): 完成游戏所需的分数
            n_lives (int): 学生拥有的生命数量,默认为 3.
        """
        self.points_to_win = points_to_win

        self.points = 0
        
        self.lives = n_lives

    def get_numeric_input(self, message=""):

        while True:
            # 获取用户输入
            user_input = input(message) 
            
            # 如果输入是数字,则返回
            # 否则,打印错误消息并重复
            if user_input.isnumeric():
                return int(user_input)
            else:
                print("输入必须是数字")
                continue     
             
    def print_welcome_message(self):
        print("PYTHON 乘法游戏".center(self.message_lenght))

    def print_lose_message(self):
        print("很遗憾,你用光了所有生命".center(self.message_lenght))

    def print_win_message(self):
        print(f"恭喜你获得了 {self.points} 分".center(self.message_lenght))
        
    def print_current_lives(self):
        print(f"当前你有 {self.lives} 条生命\n")

    def print_current_score(self):
        print(f"\n你的分数是 {self.points}")

    def print_description(self):
        print("\n\n" + self.description.center(self.message_lenght) + "\n")

    # 基本运行方法
    def run(self):
        self.print_welcome_message()
        
        self.print_description()

看起来这个类有点长。让我详细解释一下。

首先,让我们了解一下类属性和构造函数。

基本上,类属性是在类内部创建的变量,但位于构造函数或任何方法之外。

实例属性是只在构造函数内部创建的变量。

它们之间的主要区别在于作用域。类属性可以从实例对象和类中访问。实例属性只能从实例对象访问。

game = BaseGame(5)

# 从类访问 game 的 message_lenght 类属性
print(game.message_lenght) # 60

# 从类访问 message_lenght 类属性
print(BaseGame.message_lenght)  # 60

# 从实例访问 points 实例属性
print(game.points) # 0

# 从类访问 points 实例属性
print(BaseGame.points) # Attribute error

关于这个主题,我们可以在另一篇文章中更深入地探讨。请保持关注。

get_numeric_input 函数用于防止用户输入任何非数字内容。正如你所看到的,这个方法会持续询问用户,直到得到一个数字输入。我们将在子类中使用它。

打印方法使我们可以避免在每次游戏中发生事件时重复打印相同的内容。

最后但同样重要的是,run 方法只是一个封装器,随机乘法和乘法表类将使用它与用户交互并使一切正常运行。

创建子类

一旦我们创建了父类,它建立了我们应用程序的结构和一些功能,我们就可以使用继承的强大功能来构建实际的游戏模式类了。

随机乘法类

这个类将运行我们游戏的“第一种模式”。它将使用随机模块,允许我们要求用户进行 1 到 10 之间的随机运算。这里有一篇关于随机(和其他重要模块)的优秀文章😉。

import random # 用于随机操作的模块
class RandomMultiplication(BaseGame):

    description = "在这个游戏中,你必须正确回答随机乘法\n如果你达到 5 分则获胜,或者失去所有生命则失败"

    def __init__(self):
        # 赢得游戏所需的分数为 5
        # 传递参数 5 "points_to_win"
        super().__init__(5)

    def get_random_numbers(self):

        first_number = random.randint(1, 10)
        second_number = random.randint(1, 10)

        return first_number, second_number
        
    def run(self):
        
        # 调用父类来打印欢迎消息
        super().run()
        

        while self.lives > 0 and self.points_to_win > self.points:
            # 获取两个随机数
            number1, number2 = self.get_random_numbers()

            operation = f"{number1} x {number2}: "

            # 要求用户回答运算
            # 防止值错误
            user_answer = self.get_numeric_input(message=operation)

            if user_answer == number1 * number2:
                print("\n你的答案是正确的\n")
                
                # 加一分
                self.points += 1
            else:
                print("\n抱歉,你的答案不正确\n")

                # 减一条生命
                self.lives -= 1
            
            self.print_current_score()
            self.print_current_lives()
            
        # 仅当游戏结束时执行
        # 当所有条件都不满足时
        else:
            # 打印最终消息
            
            if self.points >= self.points_to_win:
                self.print_win_message()
            else:
                self.print_lose_message()

这是另一个大型的类。但正如我之前所说,重要的不是它有多少行,而是它的可读性和效率。Python 的优点在于它允许开发人员编写清晰易读的代码,就像他们用普通英语交谈一样。

这个类中有一件事可能会让你感到困惑,我会尽可能简单地解释它。

    # 父类
    def __init__(self, points_to_win, n_lives=3):
        "...
    # 子类
    def __init__(self):
        # 赢得游戏所需的分数为 5
        # 传递参数 5 "points_to_win"
        super().__init__(5)

子类的构造函数调用父类的构造函数,同时引用父类(BaseGame)。它基本上是在告诉 Python:

把父类的“points_to_win”属性设置为 5!

没有必要将 self 放入 super().__init__() 部分,仅仅因为我们在构造函数中调用了 super,这将导致冗余。

我们还在 run 方法中使用了 super 函数,我们将在该代码片段中看到发生了什么。

    # 基本运行方法
    # 父类方法
    def run(self):
        self.print_welcome_message()
        
        self.print_description()
    def run(self):
        
        # 调用父类来打印欢迎消息
        super().run()
        
        .....

你可能注意到,父类中的 run 方法会打印欢迎和描述消息。但保留该功能并在子类中添加额外的功能是一个好主意。因此,我们使用 super 在运行下一段代码之前运行父方法的所有代码。

run 函数的另一部分很简单。它要求用户输入一个数字,其中包含他/她必须回应的运算消息。然后将结果与实际乘法进行比较,如果相等,则加一分,如果不相等,则减去一条生命。

值得注意的是,我们正在使用 while-else 循环。这超出了本文的范围,但我将在几天后发表一篇关于它的文章。

最后,get_random_numbers 函数使用 random.randint 返回指定范围内的随机整数。然后它返回两个随机整数的元组。

乘法表类

“第二种模式”必须以乘法表的形式显示游戏,并确保用户至少正确完成两张表。

为此,我们将再次使用 super 的强大功能,并将父类的 points_to_win 属性修改为 2。

class TableMultiplication(BaseGame):

    description = "在这个游戏中,你必须正确解决完整的乘法表\n如果你解决了 2 张表则获胜"
    
    def __init__(self):
        # 需要完成 2 张表才能获胜
        super().__init__(2)

    def run(self):

        # 打印欢迎消息
        super().run()

        while self.lives > 0 and self.points_to_win > self.points:
            # 获取一个 1-10 的随机数
            number = random.randint(1, 10)            

            for i in range(1, 11):
                
                if self.lives <= 0:
                    # 确保在用户用光生命时,游戏无法继续
                    self.points = 0
                    break 
                
                operation = f"{number} x {i}: "

                user_answer = self.get_numeric_input(message=operation)

                if user_answer == number * i:
                    print("太棒了!你的答案是正确的")
                else:
                    print("很抱歉,你的答案不正确") 

                    self.lives -= 1

            self.points += 1
            
        # 仅当游戏结束时执行
        # 当所有条件都不满足时
        else:
            # 打印最终消息
            
            if self.points >= self.points_to_win:
                self.print_win_message()
            else:
                self.print_lose_message()

如你所见,我们只修改了此类的 run 方法。这就是继承的魔力,我们一次编写在多个地方使用的逻辑,然后就可以忘记它了😀。

在 run 方法中,我们使用 for 循环获取 1 到 10 的数字,并构建要向用户显示的运算。

如果生命耗尽或达到赢得游戏所需的分数,while 循环将再次中断,并显示输赢消息。

好的,我们已经创建了游戏的两种模式,但到目前为止,如果我们运行程序,什么也不会发生。

因此,让我们通过实现模式选择并根据该选择实例化类来完成程序。

选择模式

用户将能够选择要玩的游戏模式。下面是实现方法。

if __name__ == "__main__":

    print("选择游戏模式")

    choice = input("[1],[2]: ")

    if choice == "1":
        game = RandomMultiplication()
    elif choice == "2":
        game = TableMultiplication()
    else:
        print("请选择一个有效的游戏模式")
        exit()

    game.run()

首先,我们要求用户选择模式 1 或 2。如果输入无效,脚本将停止运行。如果用户选择第一种模式,程序将运行随机乘法游戏模式,如果他/她选择第二种模式,程序将运行乘法表模式。

这是它的样子。

结论

恭喜你刚刚使用面向对象编程构建了一个 Python 应用程序

所有代码都可以在Github 仓库中找到。

在本文中,你学到了:

  • 如何使用 Python 类构造函数
  • 如何使用 OOP 创建功能性应用程序
  • 如何在 Python 类中使用 super 函数
  • 如何应用继承的基本概念
  • 如何实现类属性和实例属性

编码愉快👨‍💻

接下来,探索一些最佳的 Python IDE,以提高你的工作效率。