关于oop:如何在Python中设计类?

How do I design a class in Python?

在我之前的问题上,我有一些非常棒的帮助,可以检测到爪内的爪和脚趾,但是所有这些解决方案一次只能用于一个测量。

现在我得到的数据包括:

  • 约30只狗;
  • 每个都有24个测量值(分为多个子组);
  • 每个测量至少有4个触点(每个棘爪一个)和
    • 每个触点分为5个部分,
    • 有几个参数,如接触时间、位置、总力等。

alt text

很明显,把所有的东西都放在一个大对象中并不能切断它,所以我想我需要使用类而不是当前的函数集合。但是,尽管我已经阅读了关于类的学习python的章节,但是我没有将它应用到我自己的代码中(github链接)

我也觉得每次我想得到一些信息时处理所有的数据是很奇怪的。一旦我知道了每个爪子的位置,就没有理由再计算这个了。此外,我想比较同一只狗的所有爪,以确定哪个接触属于哪个爪(前/后,左/右)。如果我继续只使用函数,这将变得一团糟。

因此,现在我正在寻找关于如何创建类的建议,这些类可以让我以一种合理的方式处理我的数据(链接到一只狗的压缩数据)。


如何设计一个类。

  • 把单词写下来。你开始这样做了。有些人不知道为什么会有问题。

  • 将您的一组单词展开成简单的语句,说明这些对象将要做什么。也就是说,写下你在这些事情上要做的各种计算。你列出的30只狗,24个测量值,4个接触点,以及每个接触点的几个"参数"很有趣,但只是故事的一部分。你的"每个爪的位置"和"比较同一只狗的所有爪以确定哪个接触属于哪个爪"是对象设计的下一步。

  • 给名词加下划线。说真的。有些人对这一点的价值表示怀疑,但我发现对于第一次使用OO的开发人员来说,这是有帮助的。给名词加下划线。

  • 复习名词。像"参数"和"度量"这样的一般性名词需要替换为应用于问题域中问题的特定具体名词。细节有助于澄清问题。泛型只是省略了细节。

  • 对于每个名词("contact"、"paw"、"dog"等),写下该名词的属性以及该对象所从事的动作。别抄近路。每个属性。"例如,数据集包含30只狗是很重要的。

  • 对于每个属性,确定这是与已定义名词的关系,还是与其他类型的"原始"或"原子"数据(如字符串、浮点或不可约的数据)的关系。

  • 对于每一个动作或操作,你必须确定哪一个名词有责任,哪一个名词只参与其中。这是一个"可变性"的问题。有些对象会更新,有些则不会。可变对象必须对它们的突变负全部责任。

  • 此时,可以开始将名词转换为类定义。有些集合名词是列表、字典、元组、集合或命名的两种,您不需要做很多工作。其他类更复杂,可能是因为派生数据复杂,也可能是因为执行了某些更新/突变。

  • 不要忘记使用UnitTest单独测试每个类。

    而且,没有法律规定类必须是可变的。例如,在您的案例中,几乎没有可变数据。您所拥有的是由源数据集的转换函数创建的派生数据。


    以下建议(类似于@s.lott的建议)来自于本书,开始于python:从新手到专业人士

  • Write down a description of your problem (what should the problem do?). Underline all the nouns, verbs, and adjectives.

  • Go through the nouns, looking for potential classes.

  • Go through the verbs, looking for potential methods.

  • Go through the adjectives, looking for potential attributes

  • Allocate methods and attributes to your classes

  • 为了完善课程,本书还建议我们可以做以下工作:

  • Write down (or dream up) a set of use cases—scenarios of how your program may be used. Try to cover all the functionally.

  • Think through every use case step by step, making sure that everything we need is covered.


  • 我喜欢TDD方法…因此,首先要为你想要的行为编写测试。并编写通过的代码。在这一点上,不要太担心设计,只要得到一个测试套件和通过测试的软件。如果你最终只得到一个大而难看的类,使用复杂的方法,不要担心。

    有时,在这个初始过程中,您会发现一个很难测试的行为,需要分解,只是为了测试性。这可能是一个提示,说明需要单独的类。

    那么有趣的部分…重构。有了工作软件后,您可以看到复杂的部分。通常,少量的行为会变得明显,这意味着一个新的类,但如果不是,只需寻找简化代码的方法。提取服务对象和值对象。简化你的方法。

    如果您正确使用Git(您使用的是Git,对吗?)在重构过程中,您可以非常快地对某些特定的分解进行试验,然后放弃它,如果它不能简化事情的话,则返回。

    通过首先编写经过测试的工作代码,您应该对问题域有一个非常深入的了解,而设计第一方法是不容易做到的。编写测试和代码会让你摆脱"我从哪里开始"的瘫痪。


    OO设计的全部思想是使代码映射到您的问题上,因此,例如,当您想要狗的第一步时,您可以执行如下操作:

    1
    dog.footstep(0)

    现在,对于您的情况,您可能需要读取原始数据文件并计算足迹位置。所有这些都可以隐藏在footstep()函数中,以便它只发生一次。类似:

    1
    2
    3
    4
    5
    6
    7
     class Dog:
       def __init__(self):
         self._footsteps=None
       def footstep(self,n):
         if not self._footsteps:
            self.readInFootsteps(...)
         return self._footsteps[n]

    [这现在是一种缓存模式。当它第一次读取足迹数据时,随后它只从自身获取数据。_footstep。]

    但是,是的,正确地进行OO设计是很困难的。更多地考虑您想要对数据做的事情,这将告诉您需要哪些方法来应用于哪些类。


    在略读了你链接的代码之后,在我看来,现在最好不要设计一个dog类。相反,您应该使用熊猫和数据帧。数据帧是一个带有列的表。您的数据帧将具有以下列:dog_idcontact_partcontact_timecontact_location等。熊猫在幕后使用numpy数组,它有许多方便的方法:

    • 选择一只狗,例如:my_measurements['dog_id']=='Charly'
    • 保存数据:my_measurements.save('filename.pickle')
    • 考虑使用pandas.read_csv(),而不是手动读取文本文件。

    写出你的名词、动词、形容词是一个很好的方法,但我更喜欢把课堂设计看作是在问问题:应该隐藏什么数据?

    假设你有一个Query物体和一个Database物体:

    Query对象将帮助您创建和存储一个查询——store是这里的键,因为函数可以帮助您创建一个查询。也许你可以留下来:Query().select('Country').from_table('User').where('Country =="Brazil"')。语法完全不重要——这是你的工作!--关键是对象帮助您隐藏一些东西,在本例中是存储和输出查询所需的数据。对象的力量来自于使用它的语法(在本例中是一些巧妙的链接),而不需要知道它存储了什么才能使它工作。如果执行正确,Query对象可以为多个数据库输出查询。它在内部存储特定的格式,但在输出时可以很容易地转换为其他格式(postgres、mysql、mongodb)。

    现在让我们考虑一下Database对象。这里面藏着什么?很明显,它不能存储数据库的全部内容,因为这就是为什么我们有一个数据库!那有什么意义呢?目的是隐藏数据库如何对使用Database对象的人工作。好的类将在操作内部状态时简化推理。对于这个Database对象,您可以隐藏网络调用的工作方式、批量查询或更新,或者提供缓存层。

    问题是这个Database对象很大。它表示如何访问数据库,因此在封面下它可以做任何事情。很明显,根据您的系统,网络、缓存和批处理是很难处理的,因此将它们隐藏起来会非常有帮助。但是,正如许多人会注意到的那样,数据库是非常复杂的,而且离原始的DB调用越远,就越难调整性能并理解工作原理。

    这是OOP的基本权衡。如果选择正确的抽象,它会使编码更简单(字符串、数组、字典),如果选择的抽象太大(数据库、emailmanager、networkingmanager),它可能会变得太复杂,无法真正理解它的工作方式或期望的内容。目标是隐藏复杂性,但有些复杂性是必要的。一个好的经验法则是从避免Manager对象开始,而是创建类似于structs的类——它们所做的只是保存数据,使用一些辅助方法来创建/操作数据以使您的生活更轻松。例如,在EmailManager的情况下,从一个名为sendEmail的函数开始,该函数接受一个Email对象。这是一个简单的起点,代码非常容易理解。

    对于您的示例,请考虑需要将哪些数据放在一起才能计算您要查找的内容。例如,如果你想知道一只动物走了多远,你可以上AnimalStepAnimalTrip两门课。既然每一次旅行都有所有的步骤数据,那么它应该能够弄清楚关于它的事情,也许AnimalTrip.calculateDistance()是有意义的。