【问题标题】:How to best implement a class method that is different for every object of this class如何最好地实现对此类的每个对象都不同的类方法
【发布时间】:2021-10-18 01:38:00
【问题描述】:

作为一名教师,我想为数学问题编写一个工作表生成器。 Python 应该生成数学问题及其解决方案(例如,创建一个多项式函数并计算它们的零点)。然后它会写入一个 LaTeX 输入文件并通过pdflatex 创建一个 pdf。它适用于几个问题,但现在我想对其进行概括并使其面向对象以加快进一步工作表的创建。

所以我创建了一个类Problem,其中包含几个参数和方法。但是每个Problem-instance 都应该有不同的函数来为数学问题及其解决方案创建文本(因为每个对象都是不同的数学问题)。而且我不知道我应该如何以优雅/适当的方式管理它。

目前我正在做这样的事情:

import random


class Problem_generator(object):
    def __init__(self):
        self.problemlist = []

    def create_worksheet(self):
        """ Creates the latex-document by writing the output in a LaTeX input file. For 
            this minimal example, I'll just print the outputs instead.
        """
        for problem in self.problemlist:
            text = problem.create()
            print("Problem: " + text[0])
            print("Solution: " + text[1])


class Problem(object):
    def __init__(self, problem_generator, function):
        # do some stuff like to create Tkinter-GUI-objects for every problem
        self.function = function
        problem_generator.problemlist.append(self)

    def create(self):
        return self.function()


def add_numbers():
    """ Create the problem to add two numbers. """

    a, b = random.randint(0, 100), random.randint(0, 100)
    problem_text = str(a) + " + " + str(b)
    solution_text = str(a+b)
    return problem_text, solution_text


generator = Problem_generator()
problem_1 = Problem(generator, add_numbers)

generator.create_worksheet()  # in the complete program I start this over a GUI

它工作正常,但感觉不“正确”。 我还考虑过使用create() 方法实现Problem 类,该方法只会引发未实现的错误,然后为我创建的每个问题更新create() 方法。但据我所知,这将为类的每个对象更新 create() 方法。

因此,我很乐意获得一些提示/建议,我可以如何以“优雅”的方式处理所描述的问题。

【问题讨论】:

  • 一般来说,处理这种情况的经典 OOP 方法是定义一个带有整体接口的抽象基类,也许还有一些通用的辅助方法,然后定义特定问题的“具体”子类来实现解决特定问题的方法。基本上就像@tchar 在their answer 中说明的那样。
  • 好的,所以你会建议这样做吗?还是有另一种/更好的方法?总的来说,这个程序不必是完美的,我只是一个自学/业余爱好者。但我只是想了解更多关于编程的知识并学习一种好的/干净的风格。

标签: python class oop methods


【解决方案1】:

这是我的做法:

import random
from typing import Callable, List, Tuple


ProblemCreator = Callable[[], Tuple[str, str]]  # returns (problem, solution)


class Problem:
    def __init__(self, function: ProblemCreator) -> None:
        # do some stuff like to create Tkinter-GUI-objects for every problem
        self.create = function


class ProblemGenerator:
    def __init__(self) -> None:
        self.problem_list: List[Problem] = []

    def create_worksheet(self) -> None:
        """
        Creates the latex-document by writing the output in a *.tex-file.
        For this minimal example, I'll just print the outputs instead.
        """
        for problem in self.problem_list:
            p, s = problem.create()
            print(f"Problem: {p}")
            print(f"Solution: {s}")

    def generate_problem(self, problem: ProblemCreator) -> None:
        self.problem_list.append(Problem(problem))


def add_numbers() -> Tuple[str, str]:
    """  Create the problem to add two numbers. """
    a, b = random.randint(0, 100), random.randint(0, 100)
    return f"{a} + {b}", f"{a+b}"


generator = ProblemGenerator()
generator.generate_problem(add_numbers)

generator.create_worksheet()  # in the complete program I start this over a GUI

我使用了类型注释和其他 Python 3 特性(如 f 字符串)来提高清晰度。

create 不需要成为一个方法——只需将其设为可调用属性即可(我已为该可调用属性的类型命名,ProblemCreator,因为它构成了此接口的重要部分) .同样,Problem 也不需要知道ProblemGenerator 并负责将自己添加到生成器的列表中;它只是创建了一个你不需要的循环依赖。相反,让ProblemGenerator 负责产生问题(就像它的名字一样)!

【讨论】:

  • 嗨 Samwise,感谢您的提示!让生成器形成问题似乎是一个很好的解决方案:)
【解决方案2】:

我真的没有发现 Problem_generator 类有什么用处,这就是我的做法(我添加了一个 ProblemGenerator 类,但您可以循环调用 issue.create)

通过这个你可以

  • 将问题定义为问题的子类(参见 AddTwoNumbers 类)
  • 将问题定义为函数(参见 subtract_numbers 函数)并使用问题装饰器将它们转换为 FunctionProblems(Problem 的子类)。
from abc import ABC, abstractmethod
from functools import wraps
import random
from typing import Callable, List, Tuple

random.seed(0) # for reproducability

class Problem(ABC):
    @property
    @abstractmethod
    def text(self) -> str:
        pass

    @property
    @abstractmethod
    def solution_text(self) -> str:
        pass


class AddTwoNumbers(Problem):
    def __init__(self) -> None:
        self._a = random.randint(0, 100)
        self._b = random.randint(0, 100)

    @property
    def text(self) -> str:
        return f'{self._a} + {self._b}'

    @property
    def solution_text(self) -> str:
        return str(self._a + self._b)

# If you want to define functions as problems you can do something like that
class FunctionProblem(Problem):
    def __init__(self, func: Callable[[], Tuple[str, str]]):
        self._text, self._solution_text = func()

    @property
    def text(self) -> str:
        return self._text

    @property
    def solution_text(self) -> str:
        return self._solution_text

# Define a decorator so that functions become FunctionProblems
def problem_decorator(func: Callable[[], Tuple[str, str]]) -> Callable[[], FunctionProblem]:
    @wraps(func)
    def wrapper():
        return FunctionProblem(func)
    return wrapper

@problem_decorator
def subtract_numbers() -> Tuple[str, str]:
    a, b = random.randint(0, 100), random.randint(0, 100)
    text = f'{a} - {b}'
    solution = str(a - b)
    return text, solution

# If you really want to define a ProblemGenerator
class ProblemGenerator:
    def __init__(self, *problems: Problem) -> None:
        self.problems = list(problems)

    def add_problem(self, problem: Problem) -> None:
        self.problems.append(problem)

    def create_worksheet(self) -> List[Tuple[str, str]]:
        for problem in self.problems:
            print(f'Problem text is {problem.text!r}, Solution is {problem.solution_text!r}')

generator = ProblemGenerator(AddTwoNumbers())
generator.add_problem(subtract_numbers())
generator.create_worksheet()

打印

Problem text is '49 + 97', Solution is '146'
Problem text is '53 - 5', Solution is '48'

【讨论】:

  • 嘿tchar,谢谢你的建议。我不太确定 - 当我只需要这个子类的一个对象时,为每个问题创建一个子类是否有用?一方面,问题生成器应该形成一种包装器。这样我就可以遍历所有已定义的问题。另一方面,我在问题生成器处绑定了主 GUI 窗口(及其按钮等)。我不需要这样做,但对我来说似乎是合理的。
  • 取决于您想要多大的灵活性。您可以使用问题的方法定义,但类给您更多的灵活性。不确定您正在使用它的上下文,但这是我将其编写为可扩展的方式。
  • 这里的主要问题不是看起来好不好,定义一个类问题只是为了传递一个要调用的函数,不是最好的设计你可以将函数直接传递给问题生成器并调用相反,如果您只想调用它。如果您仅以这种方式使用类,Problem.create 而不是调用该函数没有任何区别。
  • 嗯,好的。就我使用它而言,我为问题类实现了一个要选择/取消的选项,一个应该创建这个问题多少次的选项,以及一个混合所有或部分选定问题的选项。这样我就可以例如创建一个包含 2x 乘法和 4x 加法问题的工作表,但没有减法并混合在一起。此外,我创建了通过 GUI 输入问题范围的选项(例如,使用 100 到 1000 之间的数字)。我绑定了所有这些和 GUI 元素以显示/获取此选项到问题类。
  • 就我而言,上述选项应该足够了,但是如果我以后想实现其他选项,可能会更好地概括更多(我不太确定,因为我'到目前为止刚刚提出了一些较小的问题)
【解决方案3】:

正如我在评论中已经说过的那样,处理这种情况的经典 object-oriented programming (OOP) 方法是定义一个具有整体接口的抽象基类,也许还有一些通用帮助方法,然后定义特定问题的“具体”子类.这是一个基于您问题中的代码在 Python 上执行类似操作的玩具示例。如果看起来你正在创建一堆几乎相同的类。有时解决方法是子类化你的子类,但通常这只是意味着你没有很好地抽象问题......

从示例代码中的 cmets 看来,您似乎还希望您的类负责为图形用户界面 (GUI) 创建自己的 tkinter 对象。一般来说,这可能不是一个好主意——类通常应该只有一个single responsibility,这意味着尝试将其中的许多堆放在一起会使测试、调试和扩展你所拥有的东西变得更加困难,从而使事情变得非常复杂。

有一种常用的软件设计模式称为model–view–controller (MVC),它通常用于开发用户界面,将程序分成三个相互连接的组件,但将每个组件分开以降低复杂性——所以我建议你花时间研究它.

import abc
import random


class Problem(metaclass=abc.ABCMeta):
    """ Abstract base class of all mathematical problems. """

    def __init__(self):
        self.lines = []

    @abc.abstractmethod
    def create_problem(self):
        """ Creates input to be fed to the create_worksheet method. """
        ...

    def create_worksheet(self, indent=4, char=' '):
        """ Creates the latex-document by writing the output in a LaTeX input file.
            In this toy example, it just print the lines the create_problem() method
            generated.
        """
        padding = indent * char
        for line in self.lines:
            print(padding + line)


class AddNumbers(Problem):
    def __init__(self):
        super().__init__()  # Initialize base class.
        self.create_problem()  # Create subclass-specific data.

    def create_problem(self):
        a, b = random.randint(0, 100), random.randint(0, 100)
        self.lines.append(f'Problem: Add the two numbers {a} and {b} together')
        self.lines.append(f'Solution: {a+b}')


if __name__ == '__main__':

    # Create some sample Problem subclass instances.
    problems = [AddNumbers(), AddNumbers()]

    # Create worksheet the worksheet for each one.
    for i, problem in enumerate(problems, start=1):
        print(f'Worksheet {i}:')
        problem.create_worksheet(indent=2)

【讨论】:

  • 好的,感谢您的提示。我将看看 MVC 概念以了解更多项目。还有一个问题:您正在为每个问题生成一个新的子类。这样做而不是只创建之前定义的基类问题的对象有什么好处?
  • 如果你正在创建一堆几乎相同的类,尤其是它们所包含的数据不同的类,那么抽象是错误的。您要抽象的是 behavior 即实现。如果只是数据,则没有理由创建子类,因为您可以在创建单个类的实例时简单地传递不同的参数来处理它。如果您环顾四周寻找 OOP 示例,请注意它是如何在基类及其子类之间拆分的。
  • 所以你会建议将文本生成函数定义为简单函数并将它们作为参数传递给 calss 问题?
  • 是的,你可以这样做,如果类的实例之间没有任何其他差异并且文本的生成不依赖于他们——在这种情况下,为什么不将生成的文本作为参数而不是可调用的来传递呢?推迟有什么好处?
  • 我也想过这个,但就我使用这个而言,我为问题类实现了一个要选择/取消选择的选项,一个选项应该创建这个问题的次数和一个选项混合所有或部分选定的问题。这样我就可以例如创建一个包含 2x 乘法和 4x 加法问题的工作表,但没有减法并将它们混合在一起。此外,我实现了一个选项来创建多个工作表。总而言之,如果将文本作为方法生成,我认为这可以更好地实现。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-10-28
  • 1970-01-01
  • 2021-08-21
  • 1970-01-01
  • 2019-01-17
  • 2012-05-19
相关资源
最近更新 更多