Quelle est la différence entre les attributs de classe et d'instance?

132

Y a-t-il une distinction significative entre:

class A(object):
    foo = 5   # some default value

contre.

class B(object):
    def __init__(self, foo=5):
        self.foo = foo

Si vous créez de nombreuses instances, y a-t-il une différence dans les performances ou les exigences d'espace pour les deux styles? Lorsque vous lisez le code, considérez-vous que la signification des deux styles est significativement différente?

Dan Homerick
la source
1
Je viens de réaliser qu'une question similaire a été posée et répondue ici: stackoverflow.com/questions/206734/… Dois-je supprimer cette question?
Dan Homerick
2
C'est votre question, n'hésitez pas à la supprimer. Puisque c'est le vôtre, pourquoi demander l'avis de quelqu'un d'autre?
S.Lott

Réponses:

146

Au-delà des considérations de performances, il existe une différence sémantique significative . Dans le cas de l'attribut de classe, il n'y a qu'un seul objet auquel il est fait référence. Dans l'instance-attribut-ensemble-à-instanciation, il peut y avoir plusieurs objets référencés. Par exemple

>>> class A: foo = []
>>> a, b = A(), A()
>>> a.foo.append(5)
>>> b.foo
[5]
>>> class A:
...  def __init__(self): self.foo = []
>>> a, b = A(), A()
>>> a.foo.append(5)
>>> b.foo    
[]
Alex Coventry
la source
4
Seuls les types mutables sont partagés. Comme pour intet strils sont toujours attachés à chaque instance plutôt qu'à la classe.
Babu
11
@Babu: Non, intet strsont également partagés, exactement de la même manière. Vous pouvez vérifier cela facilement avec isou id. Ou regardez simplement dans chaque instance __dict__et la classe __dict__. Le fait que les types immuables soient partagés ou non n'a généralement pas beaucoup d'importance.
abarnert
17
Cependant, notez que si vous le faites a.foo = 5, dans les deux cas, vous verrez le b.fooretour []. En effet, dans le premier cas, vous écrasez l'attribut de classe a.foopar un nouvel attribut d'instance du même nom.
Konstantin Schubert
39

La différence est que l'attribut de la classe est partagé par toutes les instances. L'attribut sur une instance est unique à cette instance.

S'ils proviennent de C ++, les attributs de la classe ressemblent plus à des variables membres statiques.

Peter Shinners
la source
1
N'est-ce pas seulement les types mutables qui sont partagés? La réponse acceptée montre une liste, qui fonctionne, mais si c'est un int, cela semble être la même chose qu'une instance attr: >>> class A(object): foo = 5 >>> a, b = A(), A() >>> a.foo = 10 >>> b.foo 5
Rafe
6
@Rafe: Non, tous les types sont partagés. La raison pour laquelle vous êtes confus est que ce qui a.foo.append(5)fait muter la valeur à laquelle se a.fooréfère, a.foo = 5est en train de créer a.fooun nouveau nom pour la valeur 5. Ainsi, vous vous retrouvez avec un attribut d'instance qui cache l'attribut de classe. Essayez la même chose a.foo = 5dans la version d'Alex et vous verrez que cela b.foon'a pas changé.
abarnert
30

Voici un très bon article et résumez-le comme ci-dessous.

class Bar(object):
    ## No need for dot syntax
    class_var = 1

    def __init__(self, i_var):
        self.i_var = i_var

## Need dot syntax as we've left scope of class namespace
Bar.class_var
## 1
foo = MyClass(2)

## Finds i_var in foo's instance namespace
foo.i_var
## 2

## Doesn't find class_var in instance namespace…
## So look's in class namespace (Bar.__dict__)
foo.class_var
## 1

Et sous forme visuelle

entrez la description de l'image ici

Attribution d'attributs de classe

  • Si un attribut de classe est défini en accédant à la classe, il remplacera la valeur de toutes les instances

    foo = Bar(2)
    foo.class_var
    ## 1
    Bar.class_var = 2
    foo.class_var
    ## 2
  • Si une variable de classe est définie en accédant à une instance, elle remplacera la valeur uniquement pour cette instance . Cela remplace essentiellement la variable de classe et la transforme en une variable d'instance disponible, intuitivement, uniquement pour cette instance .

    foo = Bar(2)
    foo.class_var
    ## 1
    foo.class_var = 2
    foo.class_var
    ## 2
    Bar.class_var
    ## 1

Quand utiliseriez-vous l'attribut de classe?

  • Stockage des constantes . Comme les attributs de classe sont accessibles en tant qu'attributs de la classe elle-même, il est souvent agréable de les utiliser pour stocker des constantes spécifiques à la classe à l'échelle de la classe

    class Circle(object):
         pi = 3.14159
    
         def __init__(self, radius):
              self.radius = radius   
        def area(self):
             return Circle.pi * self.radius * self.radius
    
    Circle.pi
    ## 3.14159
    c = Circle(10)
    c.pi
    ## 3.14159
    c.area()
    ## 314.159
  • Définition des valeurs par défaut . À titre d'exemple trivial, nous pourrions créer une liste bornée (c'est-à-dire une liste qui ne peut contenir qu'un certain nombre d'éléments ou moins) et choisir d'avoir une limite par défaut de 10 éléments

    class MyClass(object):
        limit = 10
    
        def __init__(self):
            self.data = []
        def item(self, i):
            return self.data[i]
    
        def add(self, e):
            if len(self.data) >= self.limit:
                raise Exception("Too many elements")
            self.data.append(e)
    
     MyClass.limit
     ## 10
zangw
la source
19

Puisque les personnes dans les commentaires ici et dans deux autres questions marquées comme dups semblent toutes confuses à ce sujet de la même manière, je pense qu'il vaut la peine d'ajouter une réponse supplémentaire en plus de celle d' Alex Coventry .

Le fait qu'Alex attribue une valeur de type mutable, comme une liste, n'a rien à voir avec le fait que les choses soient partagées ou non. Nous pouvons le voir avec la idfonction ou l' isopérateur:

>>> class A: foo = object()
>>> a, b = A(), A()
>>> a.foo is b.foo
True
>>> class A:
...     def __init__(self): self.foo = object()
>>> a, b = A(), A()
>>> a.foo is b.foo
False

(Si vous vous demandez pourquoi j'ai utilisé object()au lieu de, disons, 5c'est pour éviter de rencontrer deux autres problèmes que je ne veux pas aborder ici; pour deux raisons différentes, des s entièrement créés séparément 5peuvent finir par être le même instance du nombre 5. Mais les object()s créés entièrement séparément ne le peuvent pas.)


Alors, pourquoi est-ce que a.foo.append(5)dans l'exemple d'Alex affecte b.foo, mais pas a.foo = 5dans mon exemple? Eh bien, essayez a.foo = 5l'exemple d'Alex et notez que cela n'affecte pas b.foonon plus .

a.foo = 5fait juste a.fooun nom pour 5. Cela n'affecte pas b.foo, ou tout autre nom pour l'ancienne valeur qui a.foofaisait référence. * C'est un peu délicat que nous créons un attribut d'instance qui cache un attribut de classe, ** mais une fois que vous obtenez cela, rien de compliqué n'est se passe ici.


Espérons que la raison pour laquelle Alex a utilisé une liste est maintenant évidente: le fait que vous puissiez muter une liste signifie qu'il est plus facile de montrer que deux variables nomment la même liste, et signifie également qu'il est plus important dans le code réel de savoir si vous avez deux listes ou deux noms pour la même liste.


* La confusion pour les personnes venant d'un langage comme C ++ est qu'en Python, les valeurs ne sont pas stockées dans des variables. Les valeurs vivent seules dans le domaine des valeurs, les variables ne sont que des noms de valeurs et l'affectation crée simplement un nouveau nom pour une valeur. Si cela vous aide, considérez chaque variable Python comme un shared_ptr<T>fichier au lieu d'un T.

** Certaines personnes en profitent en utilisant un attribut de classe comme "valeur par défaut" pour un attribut d'instance que les instances peuvent ou non définir. Cela peut être utile dans certains cas, mais cela peut aussi prêter à confusion, alors faites attention.

Abarnert
la source
0

Il y a une autre situation.

Les attributs de classe et d'instance sont Descriptor .

# -*- encoding: utf-8 -*-


class RevealAccess(object):
    def __init__(self, initval=None, name='var'):
        self.val = initval
        self.name = name

    def __get__(self, obj, objtype):
        return self.val


class Base(object):
    attr_1 = RevealAccess(10, 'var "x"')

    def __init__(self):
        self.attr_2 = RevealAccess(10, 'var "x"')


def main():
    b = Base()
    print("Access to class attribute, return: ", Base.attr_1)
    print("Access to instance attribute, return: ", b.attr_2)

if __name__ == '__main__':
    main()

Ci-dessus affichera:

('Access to class attribute, return: ', 10)
('Access to instance attribute, return: ', <__main__.RevealAccess object at 0x10184eb50>)

Le même type d'accès à l'instance via une classe ou une instance renvoie un résultat différent!

Et j'ai trouvé dans la définition de c.PyObject_GenericGetAttr, et un excellent post .

Explique

Si l'attribut se trouve dans le dictionnaire des classes qui composent. les objets MRO, puis vérifiez si l'attribut recherché pointe vers un descripteur de données (qui n'est rien de plus qu'une classe implémentant à la fois __get__les __set__méthodes et les méthodes). Si tel est le cas, résolvez la recherche d'attribut en appelant la __get__méthode du descripteur de données (lignes 28 à 33).

SimonShyu
la source