Quand dois-je utiliser des classes en Python?

177

Je programme en python depuis environ deux ans; principalement des données (pandas, mpl, numpy), mais aussi des scripts d'automatisation et de petites applications Web. J'essaie de devenir un meilleur programmeur et d'augmenter mes connaissances en python et l'une des choses qui me dérange est que je n'ai jamais utilisé de classe (en dehors de la copie de code flask aléatoire pour de petites applications Web). Je comprends généralement ce qu'ils sont, mais je n'arrive pas à comprendre pourquoi j'en aurais besoin pour une simple fonction.

Pour ajouter de la spécificité à ma question: j'écris des tonnes de rapports automatisés qui impliquent toujours d'extraire des données de plusieurs sources de données (mongo, sql, postgres, apis), d'effectuer beaucoup ou un peu de munging et de formatage de données, d'écrire les données dans csv / excel / html, envoyez-le par e-mail. Les scripts vont de ~ 250 lignes à ~ 600 lignes. Y aurait-il une raison pour moi d'utiliser des cours pour faire cela et pourquoi?

mètresk
la source
15
il n'y a rien de mal à coder sans classes si vous pouvez mieux gérer votre code. Les programmeurs POO ont tendance à exagérer les problèmes en raison des contraintes liées à la conception du langage ou à la compréhension superficielle des différents modèles.
Jason Hu

Réponses:

134

Les classes sont le pilier de la programmation orientée objet . La POO est très concernée par l'organisation du code, la réutilisabilité et l'encapsulation.

Tout d'abord, une clause de non-responsabilité: la POO est en partie en contraste avec la programmation fonctionnelle , qui est un paradigme différent beaucoup utilisé en Python. Tous ceux qui programment en Python (ou sûrement la plupart des langages) n'utilisent pas la POO. Vous pouvez faire beaucoup de choses dans Java 8 qui ne sont pas très orientées objet. Si vous ne voulez pas utiliser la POO, alors ne le faites pas. Si vous n'écrivez que des scripts uniques pour traiter des données que vous n'utiliserez plus jamais, continuez à écrire comme vous êtes.

Cependant, il existe de nombreuses raisons d'utiliser la POO.

Certaines raisons:

  • Organisation: la POO définit des moyens bien connus et standard de décrire et de définir à la fois les données et la procédure dans le code. Les données et la procédure peuvent être stockées à différents niveaux de définition (dans différentes classes), et il existe des manières standard de parler de ces définitions. Autrement dit, si vous utilisez la POO de manière standard, cela vous aidera plus tard, ainsi que les autres, à comprendre, modifier et utiliser votre code. De plus, au lieu d'utiliser un mécanisme de stockage de données complexe et arbitraire (dictés de dictés ou listes ou dictionnaires ou listes de dictés d'ensembles, ou autre), vous pouvez nommer des éléments de structures de données et s'y référer facilement.

  • État: la POO vous aide à définir et à suivre l'état. Par exemple, dans un exemple classique, si vous créez un programme qui traite des étudiants (par exemple, un programme de qualité), vous pouvez conserver toutes les informations dont vous avez besoin à leur sujet au même endroit (nom, âge, sexe, niveau scolaire, cours, notes, enseignants, pairs, régime alimentaire, besoins spéciaux, etc.), et ces données sont conservées tant que l'objet est vivant et est facilement accessible.

  • Encapsulation : Avec l'encapsulation, la procédure et les données sont stockées ensemble. Les méthodes (un terme POO pour les fonctions) sont définies juste à côté des données sur lesquelles elles opèrent et produisent. Dans un langage comme Java qui permet le contrôle d'accès , ou en Python, selon la façon dont vous décrivez votre API publique, cela signifie que les méthodes et les données peuvent être cachées à l'utilisateur. Cela signifie que si vous avez besoin ou souhaitez modifier le code, vous pouvez faire ce que vous voulez pour l'implémentation du code, mais gardez les API publiques identiques.

  • Héritage : l'héritage vous permet de définir les données et la procédure en un seul endroit (dans une classe), puis de remplacer ou d'étendre cette fonctionnalité ultérieurement. Par exemple, en Python, je vois souvent des gens créer des sous-classes dudict classe afin d'ajouter des fonctionnalités supplémentaires. Un changement courant remplace la méthode qui lève une exception lorsqu'une clé est demandée à un dictionnaire qui n'existe pas pour donner une valeur par défaut basée sur une clé inconnue. Cela vous permet d'étendre votre propre code maintenant ou plus tard, d'autoriser d'autres personnes à étendre votre code et d'étendre le code d'autres personnes.

  • Réutilisabilité: toutes ces raisons et d'autres permettent une plus grande réutilisabilité du code. Le code orienté objet vous permet d'écrire du code solide (testé) une fois, puis de le réutiliser encore et encore. Si vous avez besoin de modifier quelque chose pour votre cas d'utilisation spécifique, vous pouvez hériter d'une classe existante et écraser le comportement existant. Si vous avez besoin de changer quelque chose, vous pouvez tout changer tout en conservant les signatures de méthode publique existantes, et personne n'est le plus sage (espérons-le).

Encore une fois, il y a plusieurs raisons de ne pas utiliser la POO, et vous n'en avez pas besoin. Mais heureusement avec un langage comme Python, vous pouvez en utiliser un peu ou beaucoup, c'est à vous de décider.

Un exemple de cas d'utilisation étudiant (aucune garantie sur la qualité du code, juste un exemple):

Orienté objet

class Student(object):
    def __init__(self, name, age, gender, level, grades=None):
        self.name = name
        self.age = age
        self.gender = gender
        self.level = level
        self.grades = grades or {}

    def setGrade(self, course, grade):
        self.grades[course] = grade

    def getGrade(self, course):
        return self.grades[course]

    def getGPA(self):
        return sum(self.grades.values())/len(self.grades)

# Define some students
john = Student("John", 12, "male", 6, {"math":3.3})
jane = Student("Jane", 12, "female", 6, {"math":3.5})

# Now we can get to the grades easily
print(john.getGPA())
print(jane.getGPA())

Dict standard

def calculateGPA(gradeDict):
    return sum(gradeDict.values())/len(gradeDict)

students = {}
# We can set the keys to variables so we might minimize typos
name, age, gender, level, grades = "name", "age", "gender", "level", "grades"
john, jane = "john", "jane"
math = "math"
students[john] = {}
students[john][age] = 12
students[john][gender] = "male"
students[john][level] = 6
students[john][grades] = {math:3.3}

students[jane] = {}
students[jane][age] = 12
students[jane][gender] = "female"
students[jane][level] = 6
students[jane][grades] = {math:3.5}

# At this point, we need to remember who the students are and where the grades are stored. Not a huge deal, but avoided by OOP.
print(calculateGPA(students[john][grades]))
print(calculateGPA(students[jane][grades]))
Dantiston
la source
En raison du «rendement», l'encapsulation Python est souvent plus propre avec les générateurs et les gestionnaires de contexte qu'avec les classes.
Dmitry Rubanovich
4
@meter J'ai ajouté un exemple. J'espère que cela aide. La note ici est qu'au lieu d'avoir à compter sur les clés de vos dictés ayant le nom correct, l'interpréteur Python fait cette contrainte pour vous si vous vous trompez et vous oblige à utiliser des méthodes définies (bien que des champs non définis (bien que Java et d'autres Les langages POO ne vous permettent pas de définir des champs en dehors des classes comme Python)).
dantiston
5
@meter aussi, comme exemple d'encapsulation: disons qu'aujourd'hui cette implémentation est bonne car je n'ai besoin d'obtenir le GPA pour 50 000 étudiants de mon université qu'une fois par trimestre. Maintenant, demain, nous obtenons une subvention et devons donner la moyenne générale actuelle de chaque étudiant à chaque seconde (bien sûr, personne ne demanderait cela, mais juste pour rendre les calculs complexes). On pourrait alors "mémoriser" le GPA et ne le calculer que lorsqu'il change (par exemple, en définissant une variable dans la méthode setGrade), les autres retournent une version mise en cache. L'utilisateur utilise toujours getGPA () mais l'implémentation a changé.
dantiston
4
@dantiston, cet exemple nécessite collections.namedtuple. Vous pouvez créer un nouveau type Student = collections.namedtuple ("Student", "name, age, gender, level, grades"). Et puis vous pouvez créer des instances john = Student ("John", 12, "male", grades = {'math': 3.5}, level = 6). Notez que vous utilisez à la fois des arguments positionnels et nommés comme vous le feriez pour la création d'une classe. Il s'agit d'un type de données déjà implémenté pour vous en Python. Vous pouvez alors faire référence à john [0] ou john.name pour obtenir le 1er élément du tuple. Vous pouvez maintenant obtenir les notes de john sous le nom de john.grades.values ​​(). Et c'est déjà fait pour vous.
Dmitry Rubanovich
2
pour moi, l'encapsulation est une raison suffisante pour toujours utiliser la POO. J'ai du mal à voir que la valeur n'utilise PAS la POO pour un projet de codage de taille raisonnable. Je suppose que j'ai besoin de réponses à la question inverse :)
San Jay
23

Chaque fois que vous avez besoin de maintenir un état de vos fonctions et que cela ne peut pas être accompli avec des générateurs (fonctions qui donnent plutôt que de retourner). Les générateurs conservent leur propre état.

Si vous souhaitez remplacer l'un des opérateurs standard , vous avez besoin d'une classe.

Chaque fois que vous avez une utilisation pour un modèle de visiteur, vous aurez besoin de classes. Tous les autres modèles de conception peuvent être accomplis plus efficacement et plus proprement avec des générateurs, des gestionnaires de contexte (qui sont également mieux implémentés en tant que générateurs qu'en tant que classes) et des types POD (dictionnaires, listes et tuples, etc.).

Si vous voulez écrire du code "pythonique", vous devriez préférer les gestionnaires de contexte et les générateurs aux classes. Ce sera plus propre.

Si vous souhaitez étendre la fonctionnalité, vous serez presque toujours en mesure de l'accomplir avec confinement plutôt qu'avec héritage.

Comme toute règle, cela a une exception. Si vous souhaitez encapsuler rapidement des fonctionnalités (c'est-à-dire écrire du code de test plutôt que du code réutilisable au niveau de la bibliothèque), vous pouvez encapsuler l'état dans une classe. Ce sera simple et n'aura pas besoin d'être réutilisable.

Si vous avez besoin d'un destructeur de style C ++ (RIIA), vous ne voulez certainement PAS utiliser de classes. Vous voulez des gestionnaires de contexte.

Dmitry Rubanovich
la source
1
Les fermetures @DmitryRubanovich ne sont pas implémentées via des générateurs en Python.
Eli Korvigo
1
@DmitryRubanovich Je faisais référence à "les fermetures sont implémentées comme des générateurs en Python", ce qui n'est pas vrai. Les fermetures sont beaucoup plus flexibles. Les générateurs sont tenus de renvoyer une Generatorinstance (un itérateur spécial), tandis que les fermetures peuvent avoir n'importe quelle signature. Vous pouvez éviter les cours la plupart du temps en créant des fermetures. Et les fermetures ne sont pas simplement des «fonctions définies dans le contexte d'autres fonctions».
Eli Korvigo
3
@Eli Korvigo, en fait, les générateurs représentent un saut syntaxique significatif. Ils créent une abstraction d'une file d'attente de la même manière que les fonctions sont des abstractions d'une pile. Et la plupart des flux de données peuvent être reconstitués à partir des primitives de pile / file d'attente.
Dmitry Rubanovich
1
@DmitryRubanovich, nous parlons ici de pommes et d'oranges. Je dis que les générateurs sont utiles dans un nombre très limité de cas et ne peuvent en aucun cas être considérés comme une substitution aux callables avec état général. Vous me dites à quel point ils sont formidables, sans contredire mes propos.
Eli Korvigo
1
@Eli Korvigo, et je dis que les callables ne sont que des généralisations de fonctions. Qui sont eux-mêmes du sucre syntaxique sur le traitement des piles. Alors que les générateurs sont du sucre syntaxique sur le traitement des files d'attente. Mais c'est cette amélioration de la syntaxe qui permet de construire facilement des constructions plus compliquées et avec une syntaxe plus claire. «.next ()» n'est presque jamais utilisé, btw.
Dmitry Rubanovich
11

Je pense que tu le fais bien. Les cours sont raisonnables lorsque vous avez besoin de simuler une logique métier ou des processus difficiles de la vie réelle avec des relations difficiles. A titre d'exemple:

  • Plusieurs fonctions avec état de partage
  • Plus d'une copie des mêmes variables d'état
  • Pour étendre le comportement d'une fonctionnalité existante

Je vous suggère également de regarder cette vidéo classique

valignatev
la source
3
Il n'est pas nécessaire d'utiliser une classe lorsqu'une fonction de rappel a besoin d'un état persistant en Python. Utiliser le rendement de Python au lieu du retour rend une fonction réentrante.
Dmitry Rubanovich
4

Une classe définit une entité du monde réel. Si vous travaillez sur quelque chose qui existe individuellement et qui a sa propre logique distincte des autres, vous devez créer une classe pour cela. Par exemple, une classe qui encapsule la connectivité de la base de données.

Si ce n'est pas le cas, pas besoin de créer de classe

Ashutosh
la source
0

Cela dépend de votre idée et de votre conception. si vous êtes un bon designer, les POO sortiront naturellement sous la forme de divers modèles de conception. Pour un simple traitement au niveau du script, les POO peuvent être une surcharge. Considérez simplement les avantages de base des POO tels que réutilisables et extensibles et assurez-vous qu'ils sont nécessaires ou non. Les POO rendent les choses complexes plus simples et les choses plus simples complexes. Simplement garder les choses simples dans les deux cas en utilisant les POO ou sans utiliser les POO. ce qui est plus simple, utilisez cela.

Mohit Thakur
la source