Est-ce une bonne pratique de déclarer des variables d'instance comme Aucune dans une classe en Python?

68

Considérez la classe suivante:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

Mes collègues ont tendance à le définir comme ceci:

class Person:
    name = None
    age = None

    def __init__(self, name, age):
        self.name = name
        self.age = age

La raison principale en est que leur éditeur de choix affiche les propriétés pour l'auto-complétion.

Personnellement, je n'aime pas le dernier point, car cela n'a aucun sens qu'une classe ait ces propriétés définies None.

Lequel serait la meilleure pratique et pour quelles raisons?

Remco Haszing
la source
63
Ne laissez jamais votre IDE dicter quel code vous écrivez?
Martijn Pieters
14
Soit dit en passant: en utilisant un IDE Python approprié (par exemple PyCharm), la définition des attributs dans le __init__fournit déjà autocomplétion , etc. De plus, en utilisant Noneempêche l'IDE pour en déduire un meilleur type de l'attribut, il est donc préférable d'utiliser un défaut raisonnable au lieu (quand possible).
Bakuriu
Si c'est juste pour l'auto-complétion, vous pouvez utiliser l'indication de type, et les docstrings supplémentaires seront aussi un plus.
Dashesy
3
"Ne laissez jamais votre IDE dicter le code que vous écrivez" est une question débattue. Depuis Python 3.6, il y a des annotations en ligne et un typingmodule, qui vous permettent de fournir des astuces à l'EDI et à linter, si ce genre de chose titille votre fantaisie ...
cz
1
Ces assignations au niveau de la classe n'ont aucun effet sur le reste du code. Ils n'ont aucun effet sur self. Même si self.nameou s'ils self.agen'étaient pas assignés dans, __init__ils n'apparaissent pas dans l'instance self, ils n'apparaissent que dans la classe Person.
jolvi

Réponses:

70

J'appelle cette dernière mauvaise pratique la règle "ceci ne fait pas ce que vous pensez qu'il fait".

La position de votre collègue peut être réécrite comme suit: "Je vais créer un groupe de variables quasi-globales statiques qui ne sont jamais consultées, mais qui prennent de la place dans les tables d'espace de noms de la classe ( __dict__), juste pour que mon IDE fasse de même. quelque chose."

StarWeaver
la source
4
Un peu comme docstrings alors ;-) Bien sûr, Python a un mode pratique pour les supprimer -OO, pour ceux qui en ont besoin.
Steve Jessop
15
La place occupée est de loin la raison la moins importante pour laquelle cette convention pue. C'est une douzaine d'octets par nom (par processus). J'ai l'impression d'avoir perdu mon temps en lisant simplement la partie de votre réponse qui s'y rapporte, c'est à quel point le coût d'espace est sans importance.
8
@delnan Je suis d'accord sur le fait que la taille de la mémoire des entrées n'a pas de sens, je pensais plutôt à l'espace logique / mental occupé, à un débogage introspectif, qui nécessitait davantage de lecture et de tri, je suppose. Je ~ LL
StarWeaver
3
En fait, si vous étiez si paranoïaque à propos de l’espace, vous remarqueriez que cela économise de l’espace et fait perdre du temps. Les membres non initialisés utilisent la valeur de classe. Par conséquent, il n'y a pas beaucoup de copies du nom de variable mappé sur None (une pour chaque instance). Le coût est donc de quelques octets par classe et les économies réalisées de quelques octets par instance. Mais chaque recherche de nom de variable qui échoue coûte très peu de temps.
Jon Jay Obermark
2
Si vous vous inquiétiez de 8 octets, vous n'utiliseriez pas Python.
cz
26

1. Rendez votre code facile à comprendre

Le code est lu beaucoup plus souvent qu'écrit. Facilitez la tâche de votre mainteneur de code (il se peut que ce soit vous-même l'année prochaine).

Je ne connais aucune règle stricte, mais je préfère avoir un état d'instance futur clairement déclaré. Crasher avec un AttributeErrorest déjà assez grave. Ne pas voir clairement le cycle de vie d'un attribut d'instance est pire. La quantité de gymnastique mentale nécessaire pour rétablir les séquences d'appels possibles menant à l'attribut attribué peut facilement devenir non triviale et entraîner des erreurs.

Donc, en général, je ne définis pas seulement tout dans le constructeur, mais je m'efforce également de limiter le nombre d'attributs mutables.

2. Ne mélangez pas les membres de niveau classe et instance

Tout ce que vous définissez directement dans la classdéclaration appartient à la classe et est partagé par toutes les instances de la classe. Par exemple, lorsque vous définissez une fonction dans une classe, cela devient une méthode identique pour toutes les instances. Même chose pour les membres de données. Ceci est totalement différent des attributs d'instance que vous définissez habituellement dans __init__.

Les membres de données de niveau classe sont les plus utiles en tant que constantes:

class Missile(object):
  MAX_SPEED = 100  # all missiles accelerate up to this speed
  ACCELERATION = 5  # rate of acceleration per game frame

  def move(self):
    self.speed += self.ACCELERATION
    if self.speed > self.MAX_SPEED:
      self.speed = self.MAX_SPEED
    # ...
9000
la source
2
Oui, mais le mélange des membres de niveau classe et instance est à peu près ce que fait def . Il crée des fonctions que nous considérons comme des attributs d'objets, mais qui sont en réalité des membres de la classe. La même chose vaut pour la propriété et ses semblables. Donner l'illusion que le travail appartient aux objets quand il est vraiment médiatisé est un moyen traditionnel de ne pas devenir fou. Comment cela peut-il être si mauvais, si c'est OK pour Python lui-même.
Jon Jay Obermark
L’ordre de résolution des méthodes en Python (qui s’applique également aux membres de données) n’est pas très simple. Ce qui n’est pas trouvé au niveau instance, sera recherché au niveau classe, puis parmi les classes de base, etc. Vous pouvez en effet masquer un membre de niveau classe (données ou méthode) en affectant un membre de même instance. Mais les méthodes au niveau instance sont liées aux instances selfet n'ont pas besoin selfd'être passées, alors que les méthodes au niveau classe ne sont pas liées , sont des fonctions simples comme on le voit à la deffois et acceptent une instance comme premier argument. Donc, ce sont des choses différentes.
9000
Je pense que AttributeErrorc'est un bon signal qu'il y a un bug. Sinon, vous avaleriez le néant et obtiendriez des résultats dénués de sens. Particulièrement important dans le cas présent, où les attributs sont définis dans la __init__, un attribut manquant (mais existant au niveau de la classe) ne peut être provoqué que par un héritage erroné.
Davidmh
@Davidmh: Une erreur détectée est toujours meilleure qu'une erreur non détectée, en effet! Je dirais que si vous devez créer un attribut Noneet que cette valeur n'a pas de sens au moment de la construction de l'instance, vous avez un problème d'architecture et vous devez repenser le cycle de vie de la valeur de l'attribut ou de sa valeur initiale. Notez qu'en définissant vos attributs tôt, vous pouvez détecter un tel problème avant même d'avoir écrit le reste de la classe, sans parler de l'exécution du code.
9000
Amusement! missiles! de toute façon, je suis à peu près sûr que c'est correct de créer des vars de niveau classe et de les mélanger ... tant que le niveau de classe contient des valeurs par défaut, etc.
Erik Aronesty Le
18

Personnellement, je définis les membres dans la méthode __ init __ (). Je n'ai jamais pensé à les définir dans la partie classe. Mais ce que je fais toujours: j’initialise tous les membres de la méthode __ init__, même ceux qui n’ont pas besoin de la méthode __ init__.

Exemple:

class Person:
    def __init__(self, name, age):
        self._name = name
        self._age = age
        self._selected = None

   def setSelected(self, value):
        self._selected = value

Je pense qu'il est important de définir tous les membres au même endroit. Cela rend le code plus lisible. Que ce soit à l'intérieur de __ init __ () ou à l'extérieur, ce n'est pas si important. Mais il est important pour une équipe de s’engager plus ou moins dans le même style de codage.

Oh, et vous remarquerez peut-être que j'ai déjà ajouté le préfixe "_" aux variables membres.

ce.moi
la source
13
Vous devez définir toutes les valeurs dans le constructeur. Si la valeur était définie dans la classe elle-même, elle serait partagée entre des instances, ce qui est bien pour None, mais pas pour la plupart des valeurs. Alors ne changez rien à ce sujet. ;)
Remco Haszing
4
Les valeurs par défaut pour les paramètres et la possibilité de prendre des arguments de position et de mot-clé arbitraires le rendent beaucoup moins pénible que prévu. (Et il est en fait possible de déléguer la construction à d'autres méthodes ou même à des fonctions autonomes, donc si vous avez vraiment besoin de plusieurs envois, vous pouvez le faire aussi).
Sean Vieira
6
@Benedict Python n'a pas de contrôle d'accès. Le trait de soulignement principal est la convention acceptée pour les détails de la mise en œuvre. Voir PEP 8 .
Doval
3
@Doval Il existe une très bonne raison de préfixer les noms d'attributs avec _: Pour indiquer qu'il est privé! (Pourquoi tant de personnes dans ce fil confondent-elles ou confondent à moitié Python avec d'autres langues?)
1
Ah, alors vous faites partie de ces personnes - ou-de-fonctions-pour-toute-interaction-exposée-personnes ... Désolé de ne pas comprendre. Ce style me semble toujours excessif, mais il a une suite.
Jon Jay Obermark
11

C'est une mauvaise pratique. Vous n'avez pas besoin de ces valeurs, elles encombrent le code et peuvent provoquer des erreurs.

Considérer:

>>> class WithNone:
...   x = None
...   y = None
...   def __init__(self, x, y):
...     self.x = x
...     self.y = y
... 
>>> class InitOnly:
...   def __init__(self, x, y):
...     self.x = x
...     self.y = y
... 
>>> wn = WithNone(1,2)
>>> wn.x
1
>>> WithNone.x #Note that it returns none, no error
>>> io = InitOnly(1,2)
>>> InitOnly.x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: class InitOnly has no attribute 'x'
Daenyth
la source
J'aurais du mal à appeler cela "causer une erreur". Ce que vous voulez dire par "x" est ambigu, mais vous voudrez peut-être la valeur initiale la plus probable.
Jon Jay Obermark
J'aurais dû préciser que cela peut causer une erreur s'il n'est pas utilisé correctement.
Daenyth
Le risque le plus élevé est toujours en cours d'initialisation sur un objet. Aucun n'est vraiment assez sûr. La seule erreur que cela va causer est d’avaler des exceptions vraiment boiteuses.
Jon Jay Obermark
1
Ne pas causer d'erreurs est un problème si les erreurs auraient évité des problèmes plus graves.
Erik Aronesty le
0

J'irai avec "un peu comme docstrings, alors" et déclarerai cela sans danger, tant que c'est toujoursNone , ou une gamme étroite d'autres valeurs, toutes immuables.

Il pue l'atavisme et l'attachement excessif aux langages statiquement typés. Et ça ne sert à rien de code. Mais il a un but mineur qui reste, dans la documentation.

Il documente les noms attendus, donc si je combine le code avec quelqu'un et que l'un de nous a «nom d'utilisateur» et l'autre nom d'utilisateur, il y a un indice sur les humains que nous avons séparés et que nous n'utilisons pas les mêmes variables.

Le fait de forcer l'initialisation complète en tant que stratégie aboutit à la même chose d'une manière plus pythonique, mais s'il y a du code réel dans la __init__, cela fournit un endroit plus clair pour documenter les variables utilisées.

Évidemment, le gros problème ici est que cela pousse les gens à s’initialiser avec des valeurs autres que None, ce qui peut être mauvais:

class X:
    v = {}
x = X()
x.v[1] = 2

laisse une trace globale et ne crée pas d'instance pour x.

Mais c’est plus bizarre en Python dans son ensemble que dans cette pratique, et nous devrions déjà être paranoïaques à ce sujet.

Jon Jay Obermark
la source