如何在Python中设计类?

问题:如何在Python中设计类?

我在以前的问题中为检测爪子中的爪子脚趾提供了非常出色的帮助,但是所有这些解决方案一次只能进行一次测量。

现在,我得到的数据包括:

  • 大约30条狗;
  • 每个都有24个测量值(分为几个子组);
  • 每次测量至少有4个接触点(每只爪子一个),并且
    • 每个联系人分为5部分,
    • 具有几个参数,例如接触时间,位置,总力等。

显然,将所有内容粘贴到一个大对象中并不会减少它,因此我认为我需要使用类而不是当前的许多函数。但是,即使我已经阅读了学习Python的有关类的章节,也无法将其应用于我自己的代码(GitHub链接

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

因此,现在我正在寻找有关如何创建类的建议,这些类将使我能够以明智的方式处理我的数据(链接到一只狗的压缩数据)。

I’ve had some really awesome help on my previous questions for detecting paws and toes within a paw, but all these solutions only work for one measurement at a time.

Now I have data that consists off:

  • about 30 dogs;
  • each has 24 measurements (divided into several subgroups);
  • each measurement has at least 4 contacts (one for each paw) and
    • each contact is divided into 5 parts and
    • has several parameters, like contact time, location, total force etc.

Obviously sticking everything into one big object isn’t going to cut it, so I figured I needed to use classes instead of the current slew of functions. But even though I’ve read Learning Python’s chapter about classes, I fail to apply it to my own code (GitHub link)

I also feel like it’s rather strange to process all the data every time I want to get out some information. Once I know the locations of each paw, there’s no reason for me to calculate this again. Furthermore, I want to compare all the paws of the same dog to determine which contact belongs to which paw (front/hind, left/right). This would become a mess if I continue using only functions.

So now I’m looking for advice on how to create classes that will let me process my data (link to the zipped data of one dog) in a sensible fashion.


回答 0

如何设计类。

  1. 写下单词。您开始这样做。有些人没有,不知道为什么会有问题。

  2. 将您的词汇集扩展为有关这些对象将要做什么的简单说明。也就是说,写下您将要在这些事情上进行的各种计算。您的30条狗的清单,24条测量值,4个联系人以及每个联系人几个“参数”很有趣,但这只是故事的一部分。您的“每个爪子的位置”和“比较同一只狗的所有爪子,以确定哪个联系人属于哪个爪子”是对象设计的下一步。

  3. 在名词下划线。说真的 一些人参数这种方法的价值,但是我发现对于初次面向对象的开发人员来说,它有所帮助。在名词下划线。

  4. 查看名词。诸如“参数”和“度量”之类的通用名词需要替换为在问题域中适用于您的问题的特定,具体名词。细节有助于澄清问题。泛型只是忽略细节。

  5. 对于每个名词(“接触”,“爪子”,“狗”等),写下该名词的属性以及该对象参与的动作。不要捷径。每个属性。例如,“数据集包含30条狗”很重要。

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

  7. 对于每个动作或操作,您必须确定哪个名词有责任,哪些名词仅参与其中。这是“可变性”的问题。有些对象得到更新,而另一些则没有。可变对象必须对其突变负全部责任。

  8. 此时,您可以开始将名词转换为类定义。一些集合名词是列表,字典,元组,集合或命名元组,您不需要做很多工作。由于复杂的派生数据或执行的某些更新/变异,其他类则更为复杂。

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

另外,没有法律规定Class必须是可变的。例如,就您而言,您几乎没有可变数据。您所拥有的是派生数据,这些数据是通过转换功能从源数据集中创建的。

How to design a class.

  1. Write down the words. You started to do this. Some people don’t and wonder why they have problems.

  2. Expand your set of words into simple statements about what these objects will be doing. That is to say, write down the various calculations you’ll be doing on these things. Your short list of 30 dogs, 24 measurements, 4 contacts, and several “parameters” per contact is interesting, but only part of the story. Your “locations of each paw” and “compare all the paws of the same dog to determine which contact belongs to which paw” are the next step in object design.

  3. Underline the nouns. Seriously. Some folks debate the value of this, but I find that for first-time OO developers it helps. Underline the nouns.

  4. Review the nouns. Generic nouns like “parameter” and “measurement” need to be replaced with specific, concrete nouns that apply to your problem in your problem domain. Specifics help clarify the problem. Generics simply elide details.

  5. For each noun (“contact”, “paw”, “dog”, etc.) write down the attributes of that noun and the actions in which that object engages. Don’t short-cut this. Every attribute. “Data Set contains 30 Dogs” for example is important.

  6. For each attribute, identify if this is a relationship to a defined noun, or some other kind of “primitive” or “atomic” data like a string or a float or something irreducible.

  7. For each action or operation, you have to identify which noun has the responsibility, and which nouns merely participate. It’s a question of “mutability”. Some objects get updated, others don’t. Mutable objects must own total responsibility for their mutations.

  8. At this point, you can start to transform nouns into class definitions. Some collective nouns are lists, dictionaries, tuples, sets or namedtuples, and you don’t need to do very much work. Other classes are more complex, either because of complex derived data or because of some update/mutation which is performed.

Don’t forget to test each class in isolation using unittest.

Also, there’s no law that says classes must be mutable. In your case, for example, you have almost no mutable data. What you have is derived data, created by transformation functions from the source dataset.


回答 1

以下建议(类似于@ S.Lott的建议)来自《Beginning Python:从新手到专业》一书

  1. 写下您的问题的描述(问题应该做什么?)。在所有名词,动词和形容词下划线。

  2. 遍历名词,寻找可能的类别。

  3. 遍历动词,寻找可能的方法。

  4. 浏览形容词,寻找潜在的属性

  5. 将方法和属性分配给您的类

为了完善课堂,本书还建议我们可以执行以下操作:

  1. 写下(或构想)一组用例,以了解如何使用程序。尝试涵盖所有功能。

  2. 逐步思考每个用例,确保涵盖了我们所需的一切。

The following advices (similar to @S.Lott’s advice) are from the book, Beginning Python: From Novice to Professional

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

  2. Go through the nouns, looking for potential classes.

  3. Go through the verbs, looking for potential methods.

  4. Go through the adjectives, looking for potential attributes

  5. Allocate methods and attributes to your classes

To refine the class, the book also advises we can do the following:

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

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


回答 2

我喜欢TDD方法…因此,首先针对所需的行为编写测试。并编写通过的代码。在这一点上,不必太担心设计,只需获得通过测试的套件和软件即可。如果您最终遇到一个丑陋的类,并且使用复杂的方法,请不要担心。

有时,在此初始过程中,您会发现难以测试且需要分解的行为(仅出于可测试性)。这可能暗示需要单独的类。

然后是有趣的部分…重构。使用了可用的软件后,您可以看到复杂的部分。通常,很少有行为的迹象,这表明有一个新的类,但如果没有,则只寻找简化代码的方法。提取服务对象和值对象。简化您的方法。

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

通过首先编写经过测试的工作代码,您应该获得对问题域的深入了解,而这些问题是设计优先方法无法轻易实现的。编写测试和代码使您摆脱“我从哪里开始”的瘫痪。

I like the TDD approach… So start by writing tests for what you want the behaviour to be. And write code that passes. At this point, don’t worry too much about design, just get a test suite and software that passes. Don’t worry if you end up with a single big ugly class, with complex methods.

Sometimes, during this initial process, you’ll find a behaviour that is hard to test and needs to be decomposed, just for testability. This may be a hint that a separate class is warranted.

Then the fun part… refactoring. After you have working software you can see the complex pieces. Often little pockets of behaviour will become apparent, suggesting a new class, but if not, just look for ways to simplify the code. Extract service objects and value objects. Simplify your methods.

If you’re using git properly (you are using git, aren’t you?), you can very quickly experiment with some particular decomposition during refactoring, and then abandon it and revert back if it doesn’t simplify things.

By writing tested working code first you should gain an intimate insight into the problem domain that you couldn’t easily get with the design-first approach. Writing tests and code push you past that “where do I begin” paralysis.


回答 3

OO设计的整个思想是使您的代码映射到您的问题,因此,例如,当您想要一只狗的第一个足迹时,您可以执行以下操作:

dog.footstep(0)

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

 class Dog:
   def __init__(self):
     self._footsteps=None 
   def footstep(self,n):
     if not self._footsteps:
        self.readInFootsteps(...)
     return self._footsteps[n]

[现在这是一种缓存模式。第一次读取足迹数据,随后又从self._footsteps获取。]

但是,是的,正确设计OO设计可能很棘手。多想想您要对数据执行的操作,这将告诉您将什么方法应用于什么类。

The whole idea of OO design is to make your code map to your problem, so when, for example, you want the first footstep of a dog, you do something like:

dog.footstep(0)

Now, it may be that for your case you need to read in your raw data file and compute the footstep locations. All this could be hidden in the footstep() function so that it only happens once. Something like:

 class Dog:
   def __init__(self):
     self._footsteps=None 
   def footstep(self,n):
     if not self._footsteps:
        self.readInFootsteps(...)
     return self._footsteps[n]

[This is now a sort of caching pattern. The first time it goes and reads the footstep data, subsequent times it just gets it from self._footsteps.]

But yes, getting OO design right can be tricky. Think more about the things you want to do to your data, and that will inform what methods you’ll need to apply to what classes.


回答 4

写出您的名词,动词,形容词是一种很好的方法,但是我更倾向于将类设计看作是询问应该隐藏哪些数据的问题?

假设您有一个Query对象和一个Database对象:

Query对象将帮助您创建和存储查询-存储是此处的关键,因为函数可以帮助您轻松创建一个查询。也许您可以留下: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(收集AnimalSteps)类。既然每个Trip都具有所有Step数据,那么它应该能够弄清楚它的内容,也许AnimalTrip.calculateDistance()是有道理的。

Writing out your nouns, verbs, adjectives is a great approach, but I prefer to think of class design as asking the question what data should be hidden?

Imagine you had a Query object and a Database object:

The Query object will help you create and store a query — store, is the key here, as a function could help you create one just as easily. Maybe you could stay: Query().select('Country').from_table('User').where('Country == "Brazil"'). It doesn’t matter exactly the syntax — that is your job! — the key is the object is helping you hide something, in this case the data necessary to store and output a query. The power of the object comes from the syntax of using it (in this case some clever chaining) and not needing to know what it stores to make it work. If done right the Query object could output queries for more then one database. It internally would store a specific format but could easily convert to other formats when outputting (Postgres, MySQL, MongoDB).

Now let’s think through the Database object. What does this hide and store? Well clearly it can’t store the full contents of the database, since that is why we have a database! So what is the point? The goal is to hide how the database works from people who use the Database object. Good classes will simplify reasoning when manipulating internal state. For this Database object you could hide how the networking calls work, or batch queries or updates, or provide a caching layer.

The problem is this Database object is HUGE. It represents how to access a database, so under the covers it could do anything and everything. Clearly networking, caching, and batching are quite hard to deal with depending on your system, so hiding them away would be very helpful. But, as many people will note, a database is insanely complex, and the further from the raw DB calls you get, the harder it is to tune for performance and understand how things work.

This is the fundamental tradeoff of OOP. If you pick the right abstraction it makes coding simpler (String, Array, Dictionary), if you pick an abstraction that is too big (Database, EmailManager, NetworkingManager), it may become too complex to really understand how it works, or what to expect. The goal is to hide complexity, but some complexity is necessary. A good rule of thumb is to start out avoiding Manager objects, and instead create classes that are like structs — all they do is hold data, with some helper methods to create/manipulate the data to make your life easier. For example, in the case of EmailManager start with a function called sendEmail that takes an Email object. This is a simple starting point and the code is very easy to understand.

As for your example, think about what data needs to be together to calculate what you are looking for. If you wanted to know how far an animal was walking, for example, you could have AnimalStep and AnimalTrip (collection of AnimalSteps) classes. Now that each Trip has all the Step data, then it should be able to figure stuff out about it, perhaps AnimalTrip.calculateDistance() makes sense.


回答 5

浏览了链接的代码后,在我看来,最好不要在此时设计Dog类。相反,您应该使用Pandasdataframes。数据框是带有列的表。您数据帧都会有这样的栏目:dog_idcontact_partcontact_timecontact_location,等大熊猫在后台使用numpy的阵列,它已经为你许多方便的方法:

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

After skimming your linked code, it seems to me that you are better off not designing a Dog class at this point. Rather, you should use Pandas and dataframes. A dataframe is a table with columns. You dataframe would have columns such as: dog_id, contact_part, contact_time, contact_location, etc. Pandas uses Numpy arrays behind the scenes, and it has many convenience methods for you:

  • Select a dog by e.g. : my_measurements['dog_id']=='Charly'
  • save the data: my_measurements.save('filename.pickle')
  • Consider using pandas.read_csv() instead of manually reading the text files.