如何最好地实现对于此 class 的每个对象都不同的 class 方法

How to best implement a class method that is different for every object of this class

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

所以我用几个参数和方法创建了一个 class Problem。但是每个 Problem-instance 都应该有不同的功能来创建数学问题及其解决方案的文本(因为每个对象都是不同的数学问题)。而且我不知道我应该如何以 elegant/proper 方式管理它。

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

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 class,该方法只会引发未实现的错误,然后为我创建的每个问题更新 create() 方法。但据我所知,这将为 class.

的每个对象更新 create() 方法

所以我很乐意得到一些 tips/suggestions 如何以“优雅”的方式处理所描述的问题。

我会这样做:

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-strings)来提高清晰度。

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

我真的没有发现 Problem_generator class 有任何用处,下面是我的做法(我添加了 ProblemGenerator class 但你可以循环调用 problem.create)

使用这个你可以

  • 将问题定义为问题的子class(参见 AddTwoNumbers class)
  • 将问题定义为函数(参见 subtract_numbers 函数)并使用 problem_decorator 将它们转换为函数问题(问题的子class)。
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'

正如我在评论中所说,classic object-oriented programming (OOP) 处理这种情况的方法是定义一个抽象基础 class 与整体接口,也许还有一些通用辅助方法,然后定义特定于问题的“具体”子classes。这是一个玩具示例,根据您问题中的代码在 Python 上执行类似操作。如果看起来您正在创建一堆几乎相同的 classes。有时补救办法是 subclass 你的 subclasses,但通常这只是意味着你没有很好地抽象问题......

从您的示例代码中的评论来看,您似乎还希望 classes 负责为图形用户界面 (GUI) 创建自己的 tkinter 对象。一般来说,这可能不是一个好主意 — classes 通常应该只有一个 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)