Comment concevoir une classe en Python?


143

J'ai eu une aide vraiment formidable sur mes questions précédentes pour détecter les pattes et les orteils dans une patte , mais toutes ces solutions ne fonctionnent que pour une mesure à la fois.

Maintenant, j'ai des données qui consistent en:

  • environ 30 chiens;
  • chacun a 24 mesures (divisées en plusieurs sous-groupes);
  • chaque mesure a au moins 4 contacts (un pour chaque patte) et
    • chaque contact est divisé en 5 parties et
    • a plusieurs paramètres, comme le temps de contact, l'emplacement, la force totale, etc.

texte alternatif

De toute évidence, tout coller dans un seul gros objet ne va pas le couper, alors j'ai pensé que je devais utiliser des classes au lieu de la multitude de fonctions actuelles. Mais même si j'ai lu le chapitre Learning Python sur les classes, je ne l'applique pas à mon propre code ( lien GitHub )

J'ai aussi l'impression qu'il est assez étrange de traiter toutes les données à chaque fois que je veux sortir des informations. Une fois que je connais les emplacements de chaque patte, il n'y a aucune raison pour moi de calculer cela à nouveau. De plus, je souhaite comparer toutes les pattes d'un même chien pour déterminer quel contact appartient à quelle patte (avant / arrière, gauche / droite). Cela deviendrait un gâchis si je continue à utiliser uniquement des fonctions.

Alors maintenant, je cherche des conseils sur la façon de créer des classes qui me permettront de traiter mes données ( lien vers les données zippées d'un chien ) de manière raisonnable.


4
Vous pouvez également envisager d'utiliser une base de données (comme sqlite: docs.python.org/library/sqlite3.html ). Vous pouvez écrire un programme qui lit vos énormes fichiers de données et les convertit en lignes dans les tables de base de données. Ensuite, dans un deuxième temps, vous pouvez écrire des programmes qui extraient des données de la base de données pour effectuer une analyse plus approfondie.
unutbu

Vous voulez dire quelque chose comme je l'ai demandé ici @ubutbu? Je prévois de le faire, mais j'aimerais d'abord pouvoir traiter toutes les données de manière plus organisée
Ivo Flipse

Réponses:


434

Comment concevoir une classe.

  1. Écrit les mots. Vous avez commencé à faire ça. Certaines personnes ne le font pas et se demandent pourquoi elles ont des problèmes.

  2. Développez votre ensemble de mots en déclarations simples sur ce que ces objets vont faire. C'est-à-dire, notez les différents calculs que vous ferez sur ces choses. Votre courte liste de 30 chiens, 24 mesures, 4 contacts et plusieurs "paramètres" par contact est intéressante, mais seulement une partie de l'histoire. Vos "emplacements de chaque patte" et "comparer toutes les pattes du même chien pour déterminer quel contact appartient à quelle patte" sont la prochaine étape dans la conception d'objet.

  3. Soulignez les noms. Sérieusement. Certaines personnes débattent de la valeur de cela, mais je trouve que pour les nouveaux développeurs OO, cela aide. Soulignez les noms.

  4. Passez en revue les noms. Les noms génériques comme «paramètre» et «mesure» doivent être remplacés par des noms spécifiques et concrets qui s'appliquent à votre problème dans votre domaine de problème. Les détails aident à clarifier le problème. Les génériques élident simplement les détails.

  5. Pour chaque nom («contact», «patte», «chien», etc.), écrivez les attributs de ce nom et les actions dans lesquelles cet objet s'engage. Ne raccourcissez pas cela. Chaque attribut. «L'ensemble de données contient 30 chiens» par exemple est important.

  6. Pour chaque attribut, identifiez s'il s'agit d'une relation avec un nom défini, ou un autre type de données "primitives" ou "atomiques" comme une chaîne ou un flottant ou quelque chose d'irréductible.

  7. Pour chaque action ou opération, vous devez identifier quel nom a la responsabilité et quels noms participent simplement. C'est une question de «mutabilité». Certains objets sont mis à jour, d'autres non. Les objets mutables doivent assumer l'entière responsabilité de leurs mutations.

  8. À ce stade, vous pouvez commencer à transformer les noms en définitions de classe. Certains noms collectifs sont des listes, des dictionnaires, des tuples, des ensembles ou des tuples nommés, et vous n'avez pas besoin de faire beaucoup de travail. D'autres classes sont plus complexes, soit en raison de données dérivées complexes, soit en raison d'une mise à jour / mutation effectuée.

N'oubliez pas de tester chaque classe de manière isolée en utilisant unittest.

De plus, aucune loi ne stipule que les classes doivent être mutables. Dans votre cas, par exemple, vous n'avez pratiquement aucune donnée modifiable. Vous disposez de données dérivées, créées par des fonctions de transformation à partir de l'ensemble de données source.


24

Les conseils suivants (similaires aux conseils de @ S.Lott) sont tirés du livre Beginning Python: From Novice to Professional

  1. Écrivez une description de votre problème (que devrait faire le problème?). Soulignez tous les noms, verbes et adjectifs.

  2. Parcourez les noms à la recherche de classes potentielles.

  3. Parcourez les verbes à la recherche de méthodes potentielles.

  4. Passez en revue les adjectifs, à la recherche d'attributs potentiels

  5. Allouez des méthodes et des attributs à vos classes

Pour affiner la classe, le livre conseille également de faire ce qui suit:

  1. Écrivez (ou imaginez) un ensemble de cas d'utilisation - des scénarios d'utilisation de votre programme. Essayez de couvrir tous fonctionnellement.

  2. Réfléchissez à chaque cas d'utilisation étape par étape, en vous assurant que tout ce dont nous avons besoin est couvert.


Ce serait bien d'avoir quelques exemples du genre de phrases que nous sommes censés écrire.
endolith

14

J'aime l'approche TDD ... Alors commencez par écrire des tests pour ce que vous voulez que le comportement soit. Et écrivez du code qui passe. À ce stade, ne vous inquiétez pas trop de la conception, procurez-vous simplement une suite de tests et un logiciel qui réussit. Ne vous inquiétez pas si vous vous retrouvez avec une seule grande classe laide, avec des méthodes complexes.

Parfois, au cours de ce processus initial, vous trouverez un comportement difficile à tester et qui doit être décomposé, juste pour la testabilité. Cela peut indiquer qu'une classe distincte est justifiée.

Ensuite, la partie amusante ... refactoring. Une fois que vous avez un logiciel fonctionnel, vous pouvez voir les pièces complexes. Souvent, de petites poches de comportement deviendront apparentes, suggérant une nouvelle classe, mais sinon, cherchez simplement des moyens de simplifier le code. Extraire des objets de service et des objets de valeur. Simplifiez vos méthodes.

Si vous utilisez git correctement (vous utilisez git, n'est-ce pas?), Vous pouvez très rapidement expérimenter une décomposition particulière lors du refactoring, puis l'abandonner et revenir en arrière si cela ne simplifie pas les choses.

En écrivant d'abord un code de travail testé, vous devriez obtenir un aperçu intime du domaine du problème que vous ne pourriez pas facilement obtenir avec l'approche de la conception d'abord. L'écriture de tests et de code vous pousse au-delà de cette paralysie «par où commencer».


1
Je suis moi aussi d'accord avec cette réponse, bien que décomposer le problème et identifier les classes possibles (c'est-à-dire faire une architecture logicielle "juste assez") peut être très utile si le problème doit être travaillé en parallèle par plusieurs membres de l'équipe.
Ben Smith

3

L'idée même de la conception OO est de faire correspondre votre code à votre problème.Ainsi, lorsque, par exemple, vous voulez le premier pas d'un chien, vous faites quelque chose comme:

dog.footstep(0)

Maintenant, il se peut que pour votre cas, vous ayez besoin de lire votre fichier de données brutes et de calculer les emplacements des pas. Tout cela pourrait être caché dans la fonction footstep () pour que cela n'arrive qu'une seule fois. Quelque chose comme:

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

[C'est maintenant une sorte de modèle de mise en cache. La première fois qu'il lit les données de pas, les fois suivantes, il les obtient simplement de self._footsteps.]

Mais oui, obtenir une conception OO correcte peut être délicat. Pensez davantage à ce que vous voulez faire à vos données, et cela vous indiquera quelles méthodes vous devrez appliquer à quelles classes.


2

Ecrire vos noms, verbes, adjectifs est une excellente approche, mais je préfère penser à la conception de classe comme à la question de savoir quelles données doivent être cachées ?

Imaginez que vous ayez un Queryobjet et un Databaseobjet:

L' Queryobjet vous aidera à créer et à stocker une requête - stocker, c'est la clé ici, car une fonction pourrait vous aider à en créer une tout aussi facilement. Peut - être que vous pourriez rester: Query().select('Country').from_table('User').where('Country == "Brazil"'). Peu importe la syntaxe exacte - c'est votre travail! - la clé est que l'objet vous aide à cacher quelque chose , dans ce cas les données nécessaires pour stocker et générer une requête. La puissance de l'objet vient de la syntaxe de son utilisation (dans ce cas un enchaînement intelligent) et de ne pas avoir besoin de savoir ce qu'il stocke pour le faire fonctionner. Si cela est fait correctement, l' Queryobjet peut générer des requêtes pour plus d'une base de données. Il stockerait en interne un format spécifique mais pourrait facilement être converti en d'autres formats lors de la sortie (Postgres, MySQL, MongoDB).

Maintenant, réfléchissons à l' Databaseobjet. Qu'est-ce que cela cache et stocke? Eh bien, il est clair qu'il ne peut pas stocker le contenu complet de la base de données, car c'est pourquoi nous avons une base de données! Alors quel est le point? L'objectif est de cacher le fonctionnement de la base de données aux personnes qui utilisent l' Databaseobjet. De bonnes classes simplifieront le raisonnement lors de la manipulation de l'état interne. Pour cet Databaseobjet, vous pouvez masquer le fonctionnement des appels réseau, effectuer des requêtes ou des mises à jour par lots, ou fournir une couche de mise en cache.

Le problème est que cet Databaseobjet est ÉNORME. Il représente comment accéder à une base de données, donc sous les couvertures, il pourrait tout faire. Il est clair que la mise en réseau, la mise en cache et le traitement par lots sont assez difficiles à gérer en fonction de votre système, il serait donc très utile de les cacher. Mais, comme beaucoup de gens le remarqueront, une base de données est incroyablement complexe, et plus vous êtes éloigné des appels de base de données bruts, plus il est difficile de régler les performances et de comprendre comment les choses fonctionnent.

C'est le compromis fondamental de la POO. Si vous choisissez la bonne abstraction, cela simplifie le codage (String, Array, Dictionary), si vous choisissez une abstraction trop grande (Database, EmailManager, NetworkingManager), cela peut devenir trop complexe pour vraiment comprendre comment cela fonctionne, ou quoi attendre. Le but est de cacher la complexité , mais une certaine complexité est nécessaire. Une bonne règle de base est de commencer par éviter les Managerobjets, et à la place de créer des classes qui ressemblent à structs- tout ce qu'ils font est de contenir des données, avec quelques méthodes d'aide pour créer / manipuler les données afin de vous faciliter la vie. Par exemple, dans le cas de EmailManagerstart avec une fonction appelée sendEmailqui prend un Emailobjet. C'est un point de départ simple et le code est très facile à comprendre.

Comme pour votre exemple, réfléchissez aux données qui doivent être réunies pour calculer ce que vous recherchez. Si vous vouliez savoir jusqu'où un animal marchait, par exemple, vous pourriez avoir des classes AnimalStepand AnimalTrip(collection of AnimalSteps). Maintenant que chaque voyage a toutes les données Step, alors il devrait être capable de comprendre des choses à ce sujet, cela a peut-être du AnimalTrip.calculateDistance()sens.


2

Après avoir parcouru votre code lié, il me semble que vous feriez mieux de ne pas concevoir de classe Dog à ce stade. Vous devez plutôt utiliser des pandas et des dataframes . Un dataframe est une table avec des colonnes. Vous auriez dataframe colonnes telles que: dog_id, contact_part, contact_time, contact_location, etc. Pandas utilise des tableaux NumPy dans les coulisses, et il a beaucoup de méthodes de commodité pour vous:

  • Sélectionnez un chien par exemple: my_measurements['dog_id']=='Charly'
  • sauvegarder les données: my_measurements.save('filename.pickle')
  • Pensez à utiliser pandas.read_csv()au lieu de lire manuellement les fichiers texte.
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.