Comment déclarer les valeurs par défaut des variables d'instance en Python?

90

Dois-je donner à mes membres de classe des valeurs par défaut comme ceci:

class Foo:
    num = 1

ou comme ça?

class Foo:
    def __init__(self):
        self.num = 1

Dans cette question, j'ai découvert que dans les deux cas,

bar = Foo()
bar.num += 1

est une opération bien définie.

Je comprends que la première méthode me donnera une variable de classe alors que la seconde ne le fera pas. Cependant, si je n'ai pas besoin d'une variable de classe, mais que je dois uniquement définir une valeur par défaut pour mes variables d'instance, les deux méthodes sont-elles également bonnes? Ou l'un d'eux plus «pythonique» que l'autre?

Une chose que j'ai remarquée est que dans le didacticiel Django, ils utilisent la deuxième méthode pour déclarer des modèles. Personnellement, je pense que la deuxième méthode est plus élégante, mais j'aimerais savoir quelle est la méthode «standard».

int3
la source

Réponses:

132

En étendant la réponse de bp, je voulais vous montrer ce qu'il entendait par types immuables.

Tout d'abord, ça va:

>>> class TestB():
...     def __init__(self, attr=1):
...         self.attr = attr
...     
>>> a = TestB()
>>> b = TestB()
>>> a.attr = 2
>>> a.attr
2
>>> b.attr
1

Cependant, cela ne fonctionne que pour les types immuables (inchangeables). Si la valeur par défaut était modifiable (ce qui signifie qu'elle peut être remplacée), cela se produirait à la place:

>>> class Test():
...     def __init__(self, attr=[]):
...         self.attr = attr
...     
>>> a = Test()
>>> b = Test()
>>> a.attr.append(1)
>>> a.attr
[1]
>>> b.attr
[1]
>>> 

Notez que les deux aet bont un attribut partagé. Ceci est souvent indésirable.

C'est la manière pythonique de définir les valeurs par défaut des variables d'instance, lorsque le type est mutable:

>>> class TestC():
...     def __init__(self, attr=None):
...         if attr is None:
...             attr = []
...         self.attr = attr
...     
>>> a = TestC()
>>> b = TestC()
>>> a.attr.append(1)
>>> a.attr
[1]
>>> b.attr
[]

La raison pour laquelle mon premier extrait de code fonctionne est que, avec des types immuables, Python en crée une nouvelle instance chaque fois que vous en voulez une. Si vous deviez ajouter 1 à 1, Python crée un nouveau 2 pour vous, car l'ancien 1 ne peut pas être modifié. La raison est principalement le hachage, je crois.

Xavier Ho
la source
2
Je suis récemment tombé sur cette façon beaucoup plus simple d'implémenter les valeurs par défaut pour les attributs mutables. Écrivez simplement self.attr = attr or []dans la __init__méthode. Il obtient le même résultat (je pense) et reste clair et lisible.
Bill
4
Désolé les gars. J'ai fait plus de recherches sur la suggestion dans mon commentaire ci-dessus et apparemment ce n'est pas tout à fait la même chose et n'est pas recommandé .
Projet de loi le
Pourquoi la classe TestC a-t-elle les parenthèses vides? Est-ce un héritage Python 2?
ChrisG le
55

Les deux extraits font des choses différentes, donc ce n'est pas une question de goût mais une question de comportement correct dans votre contexte. La documentation Python explique la différence, mais voici quelques exemples:

Pièce A

class Foo:
  def __init__(self):
    self.num = 1

Cela se lie numaux instances Foo . La modification de ce champ n'est pas propagée aux autres instances.

Donc:

>>> foo1 = Foo()
>>> foo2 = Foo()
>>> foo1.num = 2
>>> foo2.num
1

Pièce B

class Bar:
  num = 1

Cela se lie numà la classe Bar . Les changements sont propagés!

>>> bar1 = Bar()
>>> bar2 = Bar()
>>> bar1.num = 2 #this creates an INSTANCE variable that HIDES the propagation
>>> bar2.num
1
>>> Bar.num = 3
>>> bar2.num
3
>>> bar1.num
2
>>> bar1.__class__.num
3

Réponse réelle

Si je n'ai pas besoin d'une variable de classe, mais que je dois uniquement définir une valeur par défaut pour mes variables d'instance, les deux méthodes sont-elles également bonnes? Ou l'un d'eux plus «pythonique» que l'autre?

Le code de la pièce B est tout simplement faux pour ceci: pourquoi voudriez-vous lier un attribut de classe (valeur par défaut lors de la création de l'instance) à l'instance unique?

Le code de la pièce A est correct.

Si vous voulez donner des valeurs par défaut pour des variables d'instance dans votre constructeur, je ferais cependant ceci:

class Foo:
  def __init__(self, num = None):
    self.num = num if num is not None else 1

...ou même:

class Foo:
  DEFAULT_NUM = 1
  def __init__(self, num = None):
    self.num = num if num is not None else DEFAULT_NUM

... ou même: (préférable, mais si et seulement si vous avez affaire à des types immuables!)

class Foo:
  def __init__(self, num = 1):
    self.num = num

De cette façon, vous pouvez faire:

foo1 = Foo(4)
foo2 = Foo() #use default
badp
la source
1
La méthode init utilisée dans cet exemple est-elle différente de underscore___init__underscore pour l'initialisation @badp
Eliethesaiyan
Le premier exemple ne devrait-il pas êtredef __init__(self, num = None)
PyWalker2797
6

L'utilisation de membres de classe pour donner des valeurs par défaut fonctionne très bien tant que vous faites attention à ne le faire qu'avec des valeurs immuables. Si vous essayez de le faire avec une liste ou un dict, ce serait assez mortel. Cela fonctionne également lorsque l'attribut d'instance est une référence à une classe tant que la valeur par défaut est None.

J'ai vu cette technique utilisée avec beaucoup de succès dans le repoze qui est un framework qui fonctionne au-dessus de Zope. L'avantage ici n'est pas seulement que lorsque votre classe est conservée dans la base de données, seuls les attributs non par défaut doivent être enregistrés, mais également lorsque vous devez ajouter un nouveau champ dans le schéma, tous les objets existants voient le nouveau champ avec sa valeur par défaut value sans qu'il soit nécessaire de modifier réellement les données stockées.

Je trouve que cela fonctionne aussi bien dans le codage plus général, mais c'est une question de style. Utilisez ce qui vous plaît le plus.

Duncan
la source
3

Utiliser des membres de classe pour les valeurs par défaut des variables d'instance n'est pas une bonne idée, et c'est la première fois que je vois cette idée mentionnée du tout. Cela fonctionne dans votre exemple, mais cela peut échouer dans de nombreux cas. Par exemple, si la valeur est mutable, sa mutation sur une instance non modifiée modifiera la valeur par défaut:

>>> class c:
...     l = []
... 
>>> x = c()
>>> y = c()
>>> x.l
[]
>>> y.l
[]
>>> x.l.append(10)
>>> y.l
[10]
>>> c.l
[10]
Max Shawabkeh
la source
sauf que le tout premier a []été cloné là-bas; x.let y.lsont à la fois de mauvais noms et des valeurs immédiates différentes liées au même référent. (Termes de l'avocat de langue
inventés
1

Vous pouvez également déclarer les variables de classe comme Aucune, ce qui empêchera la propagation. Ceci est utile lorsque vous avez besoin d'une classe bien définie et que vous souhaitez éviter les erreurs d'attribut. Par exemple:

>>> class TestClass(object):
...     t = None
... 
>>> test = TestClass()
>>> test.t
>>> test2 = TestClass()
>>> test.t = 'test'
>>> test.t
'test'
>>> test2.t
>>>

Aussi si vous avez besoin de valeurs par défaut:

>>> class TestClassDefaults(object):
...    t = None
...    def __init__(self, t=None):
...       self.t = t
... 
>>> test = TestClassDefaults()
>>> test.t
>>> test2 = TestClassDefaults([])
>>> test2.t
[]
>>> test.t
>>>

Bien sûr, suivez toujours les informations des autres réponses sur l'utilisation des types mutables vs immuables par défaut dans __init__.

Réaliste
la source
0

Avec les classes de données , une fonctionnalité ajoutée dans Python 3.7, il existe maintenant un autre moyen (assez pratique) de définir des valeurs par défaut sur les instances de classe. Le décorateur dataclassgénérera automatiquement quelques méthodes sur votre classe, comme le constructeur. Comme le note la documentation liée ci-dessus, "[l] es variables membres à utiliser dans ces méthodes générées sont définies à l'aide d' annotations de type PEP 526 ".

En prenant l'exemple d'OP, nous pourrions l'implémenter comme ceci:

from dataclasses import dataclass

@dataclass
class Foo:
    num: int = 0

Lors de la construction d'un objet du type de cette classe, nous pourrions éventuellement écraser la valeur.

print('Default val: {}'.format(Foo()))
# Default val: Foo(num=0)
print('Custom val: {}'.format(Foo(num=5)))
# Custom val: Foo(num=5)
Johannes Gehrs
la source