web-dev-qa-db-fra.com

Définition d'attributs par défaut / vides pour les classes d'utilisateurs dans __init__

J'ai un niveau de programmation décent et j'apporte beaucoup de valeur à la communauté ici. Cependant, je n'ai jamais eu beaucoup d'enseignement académique en programmation ni travaillé à côté de programmeurs vraiment expérimentés. Par conséquent, j'ai parfois du mal avec les "meilleures pratiques".

Je ne peux pas trouver un meilleur endroit pour cette question, et je le poste malgré les incendiaires probables qui détestent ce genre de questions. Désolé si cela vous dérange. J'essaie juste d'apprendre, pas de te faire chier.

Question:

Lorsque je crée une nouvelle classe, dois-je définir tous les attributs d'instance dans init, même s'ils sont None et en fait, des valeurs affectées ultérieurement dans les méthodes de classe?

Voir l'exemple ci-dessous pour les résultats d'attribut de MyClass:

class MyClass:
    def __init__(self,df):
          self.df = df
          self.results = None

    def results(df_results):
         #Imagine some calculations here or something
         self.results = df_results

J'ai trouvé dans d'autres projets, les attributs de classe peuvent être enterrés lorsqu'ils n'apparaissent que dans les méthodes de classe et il y a beaucoup de choses.

Donc, pour un programmeur professionnel expérimenté, quelle est la pratique standard pour cela? Pourriez-vous définir tous les attributs d'instance dans init pour plus de lisibilité?

Et si quelqu'un a des liens vers des documents sur où je peux trouver de tels principes, veuillez les mettre dans une réponse, ce sera très apprécié. Je connais PEP-8 et j'ai déjà cherché ma question ci-dessus plusieurs fois, et je ne trouve personne qui y touche.

Merci

Andy

3
Andy

Pour comprendre l'importance (ou non) de l'initialisation des attributs dans __init__, Prenons comme exemple une version modifiée de votre classe MyClass. Le but de la classe est de calculer la note d'une matière, compte tenu du nom et du score de l'élève. Vous pouvez suivre dans un interpréteur Python.

>>> class MyClass:
...     def __init__(self,name,score):
...         self.name = name
...         self.score = score
...         self.grade = None
...
...     def results(self, subject=None):
...         if self.score >= 70:
...             self.grade = 'A'
...         Elif 50 <= self.score < 70:
...             self.grade = 'B'
...         else:
...             self.grade = 'C'
...         return self.grade

Cette classe nécessite deux arguments positionnels name et score. Ces arguments must doivent être fournis pour initialiser une instance de classe. Sans ceux-ci, l'objet de classe x ne peut pas être instancié et un TypeError sera levé:

>>> x = MyClass()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() missing 2 required positional arguments: 'name' and 'score'

À ce stade, nous comprenons que nous devons fournir le name de l'étudiant et un score pour un sujet au minimum, mais le grade n'est pas important pour le moment car cela sera calculé ultérieurement, dans la méthode results. Donc, nous utilisons simplement self.grade = None Et ne le définissons pas comme un argument positionnel. Initialisons une instance de classe (objet):

>>> x = MyClass(name='John', score=70)
>>> x
<__main__.MyClass object at 0x000002491F0AE898>

Le <__main__.MyClass object at 0x000002491F0AE898> Confirme que l'objet de classe x a été créé avec succès à l'emplacement de mémoire donné. Maintenant, Python fournit des méthodes intégrées utiles pour afficher les attributs de l'objet de classe créé. L'une des méthodes est __dict__. Vous pouvez en savoir plus à ce sujet ici :

>>> x.__dict__
{'name': 'John', 'score': 70, 'grade': None}

Cela donne clairement une vue dict de tous les attributs initiaux et de leurs valeurs. Notez que grade a une valeur None comme assignée dans __init__.

Prenons un moment pour comprendre ce que fait __init__. Il existe de nombreuses réponses et ressources en ligne disponibles pour expliquer ce que fait cette méthode, mais je vais résumer:

Comme __init__, Python a une autre méthode intégrée appelée __new__(). Lorsque vous créez un objet de classe comme celui-ci x = MyClass(name='John', score=70), = Python appelle en interne __new__() d'abord pour créer une nouvelle instance de la classe MyClass puis appelle __init__ Pour initialiser les attributs name et score. Bien sûr, dans ces appels internes lorsque Python ne trouve pas les valeurs des arguments positionnels requis, il génère une erreur comme nous l'avons vu ci-dessus. En d'autres termes, __init__ Initialise les attributs. Vous pouvez affecter de nouvelles valeurs initiales pour name et score comme ceci:

>>> x.__init__(name='Tim', score=50)
>>> x.__dict__
{'name': 'Tim', 'score': 50, 'grade': None}

Il est également possible d'accéder à des attributs individuels comme ci-dessous. grade ne donne rien car c'est None.

>>> x.name
'Tim'
>>> x.score
50
>>> x.grade
>>>

Dans la méthode results, vous remarquerez que la "variable" subject est définie comme None, un argument positionnel. La portée de cette variable se trouve uniquement dans cette méthode. À des fins de démonstration, je définis explicitement subject à l'intérieur de cette méthode mais cela pourrait aussi avoir été initialisé dans __init__. Mais que faire si j'essaie d'y accéder avec mon objet:

>>> x.subject
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'MyClass' object has no attribute 'subject'

Python lève un AttributeError lorsqu'il ne trouve pas d'attribut dans l'espace de noms de la classe. Si vous n'initialisez pas d'attributs dans __init__, Il est possible de rencontrer cette erreur lorsque vous accédez à un attribut non défini qui pourrait être local à la méthode d'une classe uniquement. Dans cet exemple, définir subject à l'intérieur de __init__ Aurait évité la confusion et aurait été tout à fait normal de le faire car cela n'est pas non plus requis pour tout calcul.

Maintenant, appelons results et voyons ce que nous obtenons:

>>> x.results()
'B'
>>> x.__dict__
{'name': 'Tim', 'score': 50, 'grade': 'B'}

Cela imprime la note du score et remarque lorsque nous affichons les attributs, le grade a également été mis à jour. Dès le début, nous avions une vision claire des attributs initiaux et de la façon dont leurs valeurs ont changé.

Mais qu'en est-il de subject? Si je veux savoir combien Tim a marqué en mathématiques et quelle était la note, je peux facilement accéder au score et au grade comme nous l'avons vu auparavant, mais comment puis-je connaître le sujet? Depuis, la variable subject est locale à la portée de la méthode results, nous pourrions simplement return la valeur de subject. Modifiez l'instruction return dans la méthode results:

def results(self, subject=None):
    #<---code--->
    return self.grade, subject

Appelons à nouveau results(). Nous obtenons un tuple avec la note et le sujet comme prévu.

>>> x.results(subject='Math')
('B', 'Math')

Pour accéder aux valeurs du Tuple, attribuons-les à des variables. En Python, il est possible d'affecter des valeurs d'une collection à plusieurs variables dans la même expression, à condition que le nombre de variables soit égal à la longueur de la collection. Ici, la longueur n'est que de deux, nous pouvons donc avoir deux variables à gauche de l'expression:

>>> grade, subject = x.results(subject='Math')
>>> subject
'Math'

Donc, nous l'avons, bien qu'il ait fallu quelques lignes de code supplémentaires pour obtenir le subject. Il serait plus intuitif d'accéder à tous à la fois en utilisant uniquement l'opérateur point pour accéder aux attributs avec x.<attribute>, Mais ce n'est qu'un exemple et vous pouvez l'essayer avec subject initialisé en __init__.

Ensuite, considérez qu'il y a beaucoup d'élèves (disons 3) et nous voulons les noms, les scores, les notes pour les mathématiques. À l'exception du sujet, tous les autres doivent être une sorte de type de données de collection comme un list qui peut stocker tous les noms, scores et notes. Nous pourrions simplement initialiser comme ceci:

>>> x = MyClass(name=['John', 'Tom', 'Sean'], score=[70, 55, 40])
>>> x.name
['John', 'Tom', 'Sean']
>>> x.score
[70, 55, 40]

Cela semble bien à première vue, mais quand vous jetez un autre regard (ou un autre programmeur) sur l'initialisation de name, score et grade dans __init__, il n'y a aucun moyen de dire qu'ils ont besoin d'un type de données de collecte. Les variables sont également nommées singulières, ce qui rend plus évident qu'elles peuvent être uniquement des variables aléatoires qui peuvent ne nécessiter qu'une seule valeur. Le but des programmeurs devrait être de rendre l'intention aussi claire que possible, au moyen de la dénomination des variables descriptives, des déclarations de type, des commentaires de code, etc. Dans cet esprit, modifions les déclarations d'attribut dans __init__. Avant de nous contenter d'une déclaration bien comportée, bien définie, nous devons prendre soin de la façon dont nous déclarons les arguments par défaut.


Edit : Problèmes avec les arguments par défaut modifiables:

Maintenant, il y a des "accrochages" dont nous devons être conscients lors de la déclaration des arguments par défaut. Considérez la déclaration suivante qui initialise names et ajoute un nom aléatoire lors de la création de l'objet. Rappelons que les listes sont des objets mutables en Python.

#Not recommended
class MyClass:
    def __init__(self,names=[]):
        self.names = names
        self.names.append('Random_name')

Voyons ce qui se passe lorsque nous créons des objets à partir de cette classe:

>>> x = MyClass()
>>> x.names
['Random_name']
>>> y = MyClass()
>>> y.names
['Random_name', 'Random_name']

La liste continue de s'allonger à chaque nouvelle création d'objet. La raison derrière cela est que les valeurs par défaut sont toujours évaluées chaque fois que __init__ Est appelé. Appeler __init__ Plusieurs fois, continue à utiliser le même objet fonction, s'ajoutant ainsi à l'ensemble précédent de valeurs par défaut. Vous pouvez le vérifier vous-même car le id reste le même pour chaque création d'objet.

>>> id(x.names)
2513077313800
>>> id(y.names)
2513077313800

Alors, quelle est la bonne façon de définir les arguments par défaut tout en étant explicite sur le type de données pris en charge par l'attribut? L'option la plus sûre consiste à définir les arguments par défaut sur None et à les initialiser sur une liste vide lorsque les valeurs d'arg sont None. Voici une méthode recommandée pour déclarer les arguments par défaut:

#Recommended
>>> class MyClass:
...     def __init__(self,names=None):
...         self.names = names if names else []
...         self.names.append('Random_name')

Examinons le comportement:

>>> x = MyClass()
>>> x.names
['Random_name']
>>> y = MyClass()
>>> y.names
['Random_name']

Maintenant, ce comportement est ce que nous recherchons. L'objet ne "reporte" pas les anciens bagages et se réinitialise dans une liste vide chaque fois qu'aucune valeur n'est transmise à names. Si nous passons des noms valides (comme une liste bien sûr) à l'argument names arg pour l'objet y, Random_name Sera simplement ajouté à cette liste. Et encore une fois, les valeurs de l'objet x ne seront pas affectées:

>>> y = MyClass(names=['Viky','Sam'])
>>> y.names
['Viky', 'Sam', 'Random_name']
>>> x.names
['Random_name']

Peut-être, l'explication la plus simple sur ce concept peut également être trouvée sur le site Web Effbot . Si vous souhaitez lire d'excellentes réponses: "Le moindre étonnement" et l'argument par défaut de Mutable .


Sur la base de la brève discussion sur les arguments par défaut, nos déclarations de classe seront modifiées pour:

class MyClass:
    def __init__(self,names=None, scores=None):
        self.names = names if names else []
        self.scores = scores if scores else []
        self.grades = []
#<---code------>

Cela a plus de sens, toutes les variables ont des noms pluriels et initialisées à des listes vides lors de la création d'objets. Nous obtenons des résultats similaires comme précédemment:

>>> x.names
['John', 'Tom', 'Sean']
>>> x.grades
[]

grades est une liste vide indiquant clairement que les notes seront calculées pour plusieurs étudiants lorsque results() est appelée. Par conséquent, notre méthode results doit également être modifiée. Les comparaisons que nous faisons doivent maintenant être entre les numéros de score (70, 50 etc.) et les éléments de la liste self.scores Et alors que cela le fait, la liste self.grades Doit également être mise à jour avec l'individu. grades. Modifiez la méthode results en:

def results(self, subject=None):
    #Grade calculator 
    for i in self.scores:
        if i >= 70:
            self.grades.append('A')
        Elif 50 <= i < 70:
            self.grades.append('B')
        else:
            self.grades.append('C')
    return self.grades, subject

Nous devrions maintenant obtenir les notes sous forme de liste lorsque nous appelons results():

>>> x.results(subject='Math')
>>> x.grades
['A', 'B', 'C']
>>> x.names
['John', 'Tom', 'Sean']
>>> x.scores
[70, 55, 40]

Cela a l'air bien, mais imaginez si les listes étaient grandes et déterminer qui est le score/grade appartient à qui serait un cauchemar absolu. C'est là qu'il est important d'initialiser les attributs avec le type de données correct qui peut stocker tous ces éléments de manière à ce qu'ils soient facilement accessibles et montrent clairement leurs relations. Le meilleur choix ici est un dictionnaire.

Nous pouvons avoir un dictionnaire avec des noms et des scores définis initialement et la fonction results devrait tout rassembler dans un nouveau dictionnaire qui a tous les scores, les notes etc. Nous devrions également commenter le code correctement et définir explicitement les arguments dans le dans la mesure du possible. Enfin, il se peut que nous n'ayons plus besoin de self.grades Dans __init__ Car, comme vous le constaterez, les notes ne sont pas ajoutées à une liste mais explicitement attribuées. Cela dépend totalement des exigences du problème.

Le code final :

class MyClass:
"""A class that computes the final results for students"""

    def __init__(self,names_scores=None):

        """initialize student names and scores
        :param names_scores: accepts key/value pairs of names/scores
                         E.g.: {'John': 70}"""

        self.names_scores = names_scores if names_scores else {}     

    def results(self, _final_results={}, subject=None):
        """Assign grades and collect final results into a dictionary.

       :param _final_results: an internal arg that will store the final results as dict. 
                              This is just to give a meaningful variable name for the final results."""

        self._final_results = _final_results
        for key,value in self.names_scores.items():
            if value >= 70:
                self.names_scores[key] = [value,subject,'A']
            Elif 50 <= value < 70:
                self.names_scores[key] = [value,subject,'B']
            else:
                self.names_scores[key] = [value,subject,'C']
        self._final_results = self.names_scores #assign the values from the updated names_scores dict to _final_results
        return self._final_results

Veuillez noter que _final_results N'est qu'un argument interne qui stocke le dict mis à jour self.names_scores. Le but est de renvoyer une variable plus significative de la fonction qui informe clairement l'intention . Le _ Au début de cette variable indique qu'il s'agit d'une variable interne, selon la convention.

Donnons à ceci une dernière course:

>>> x = MyClass(names_scores={'John':70, 'Tom':50, 'Sean':40})
>>> x.results(subject='Math')  

  {'John': [70, 'Math', 'A'],
 'Tom': [50, 'Math', 'B'],
 'Sean': [40, 'Math', 'C']}

Cela donne une vue beaucoup plus claire des résultats pour chaque élève. Il est désormais facile d'accéder aux notes/scores pour tout étudiant:

>>> y = x.results(subject='Math')
>>> y['John']
[70, 'Math', 'A']

Conclusion :

Alors que le code final avait besoin d'un travail supplémentaire, mais cela en valait la peine. Le résultat est plus précis et donne des informations claires sur les résultats de chaque élève. Le code est plus lisible et informe clairement le lecteur de l'intention de créer la classe, les méthodes et les variables. Voici les principaux points à retenir de cette discussion:

  • Les variables (attributs) qui devraient être partagées entre les méthodes de classe, doivent être définies dans __init__. Dans notre exemple, names, scores et éventuellement subject étaient requis par results(). Ces attributs pourraient être partagés par une autre méthode comme say average qui calcule la moyenne des scores.
  • Les attributs doivent être initialisés avec le type de données approprié. Cela doit être décidé à l'avance avant de s'aventurer dans une conception basée sur une classe pour un problème.
  • Des précautions doivent être prises lors de la déclaration des attributs avec des arguments par défaut. Les arguments par défaut modifiables peuvent muter les valeurs de l'attribut si __init__ Englobant provoque une mutation de l'attribut à chaque appel. Il est plus sûr de déclarer les arguments par défaut comme None et de les réinitialiser dans une collection mutable vide plus tard chaque fois que la valeur par défaut est None.
  • Les noms d'attribut doivent être sans ambiguïté, suivez les directives PEP8.
  • Certaines variables doivent être initialisées uniquement dans le cadre de la méthode de classe. Il peut s'agir, par exemple, de variables internes requises pour les calculs ou de variables qui n'ont pas besoin d'être partagées avec d'autres méthodes.
  • Une autre raison convaincante de définir des variables dans __init__ Est d'éviter les AttributeErrors qui peuvent se produire en raison de l'accès à des attributs sans nom/hors de portée. La méthode intégrée __dict__ Fournit une vue des attributs initialisés ici.
  • Lors de l'attribution de valeurs aux attributs (arguments positionnels) lors de l'instanciation de classe, les noms d'attribut doivent être définis explicitement. Par exemple:

    x = MyClass('John', 70)  #not explicit
    x = MyClass(name='John', score=70) #explicit
    
  • Enfin, l'objectif devrait être de communiquer l'intention aussi clairement que possible avec des commentaires. La classe, ses méthodes et ses attributs doivent être bien commentés. Pour tous les attributs, une brève description accompagnée d'un exemple est très utile pour un nouveau programmeur qui rencontre votre classe et ses attributs pour la première fois.

0
amanb