web-dev-qa-db-fra.com

Pourquoi l'ajout d'attributs à un objet déjà instancié est-il autorisé?

J'étudie le python, et bien que je pense avoir tout le concept et la notion de Python, aujourd'hui je suis tombé sur un morceau de code que je ne comprenais pas complètement:

Disons que j'ai une classe qui est censée définir des cercles mais qui n'a pas de corps:

class Circle():
    pass

Puisque je n'ai défini aucun attribut, comment puis-je faire ceci:

my_circle = Circle()
my_circle.radius = 12

La partie bizarre est que Python accepte la déclaration ci-dessus. Je ne comprends pas pourquoi Python ne soulève pas un undefined name error. Je comprends que via la frappe dynamique je lie simplement des variables aux objets quand je le souhaite, mais ne devrait-il pas y avoir d'attribut radius dans le Circle classe pour me permettre de faire ça?

MODIFIER : Beaucoup d'informations merveilleuses dans vos réponses! Merci à tous pour toutes ces réponses fantastiques! C'est dommage que je ne puisse en marquer qu'une comme réponse.

53
NlightNFotis

Un principe directeur est que il n'y a pas de déclaration . Autrement dit, vous ne déclarez jamais "cette classe a une méthode foo" ou "les instances de cette classe ont une barre d'attribut", sans parler de faire une déclaration sur les types d'objets à y stocker. Vous définissez simplement une méthode, un attribut, une classe, etc. et il est ajouté. Comme le souligne JBernardo, tout __init__ la méthode fait exactement la même chose. Cela n'aurait pas beaucoup de sens de restreindre arbitrairement la création de nouveaux attributs aux méthodes portant le nom __init__. Et il est parfois utile de stocker une fonction en tant que __init__ qui n'ont pas réellement ce nom (par exemple les décorateurs), et une telle restriction briserait cela.

Maintenant, ce n'est pas universellement vrai. Les types intégrés omettent cette capacité comme optimisation. Via __slots__ , vous pouvez également empêcher cela sur les classes définies par l'utilisateur. Mais ce n'est qu'une optimisation de l'espace (pas besoin d'un dictionnaire pour chaque objet), pas une chose d'exactitude.

Si vous voulez un filet de sécurité, tant pis. Python n'en offre pas, et vous ne pouvez pas raisonnablement en ajouter un, et plus important encore, il serait évité par Python qui embrassent le langage (lire: presque tous ceux avec lesquels vous voulez travailler.) Les tests et la discipline contribuent toujours à garantir l'exactitude. N'utilisez pas la liberté de créer des attributs en dehors de __init__ si cela peut être évité , et faites des tests automatisés. J'ai très rarement un AttributeError ou une erreur logique due à une supercherie comme celle-ci, et parmi ceux qui se produisent, presque tous sont détectés par des tests.

47
user395760

Juste pour clarifier certains malentendus dans les discussions ici. Ce code:

class Foo(object):
    def __init__(self, bar):
        self.bar = bar

foo = Foo(5)

Et ce code:

class Foo(object):
    pass

foo = Foo()
foo.bar = 5

est exactement équivalent. Il n'y a vraiment aucune différence. Cela fait exactement la même chose. Cette différence est que dans le premier cas, il est encapsulé et il est clair que l'attribut bar est une partie normale des objets de type Foo. Dans le second cas, il n'est pas clair que ce soit le cas.

Dans le premier cas, vous ne pouvez pas créer un objet Foo qui n'a pas l'attribut bar (enfin, vous pouvez probablement, mais pas facilement), dans le second cas, les objets Foo n'auront pas d'attribut bar à moins que vous ne le définissiez.

Ainsi, bien que le code soit équivalent par programme, il est utilisé dans différents cas.

35
Lennart Regebro

Python vous permet de stocker des attributs de n'importe quel nom sur pratiquement n'importe quelle instance (ou classe, d'ailleurs). Il est possible de bloquer cela soit en écrivant la classe en C, comme les types intégrés, soit en utilisant __slots__ qui n'autorise que certains noms.

La raison pour laquelle cela fonctionne est que la plupart des instances stockent leurs attributs dans un dictionnaire. Oui, un dictionnaire Python comme vous le définiriez avec {}. Le dictionnaire est stocké dans un attribut d'instance appelé __dict__. En fait, certains disent que "les classes ne sont que du sucre syntaxique pour les dictionnaires". Autrement dit, vous pouvez faire tout ce que vous pouvez faire avec une classe avec un dictionnaire; les classes le rendent plus facile.

Vous êtes habitué aux langages statiques où vous devez définir tous les attributs au moment de la compilation. En Python, les définitions de classe sont exécutées, non compilées; les classes sont des objets comme les autres; et l'ajout d'attributs est aussi simple que l'ajout d'un élément à un dictionnaire. C'est pourquoi Python est considéré comme un langage dynamique.

19
kindall

Non, python est flexible comme ça, il n'applique pas les attributs que vous pouvez stocker sur les classes définies par l'utilisateur.

Il existe cependant une astuce, en utilisant __slots__ attribute sur une définition de classe vous empêchera de créer des attributs supplémentaires non définis dans le __slots__ séquence:

>>> class Foo(object):
...     __slots__ = ()
... 
>>> f = Foo()
>>> f.bar = 'spam'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Foo' object has no attribute 'bar'
>>> class Foo(object):
...     __slots__ = ('bar',)
... 
>>> f = Foo()
>>> f.bar
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: bar
>>> f.bar = 'spam'
17
Martijn Pieters

Il crée un membre de données radius de my_circle.

Si vous l'aviez demandé my_circle.radius cela aurait levé une exception:

>>> print my_circle.radius # AttributeError

Fait intéressant, cela ne change pas la classe; juste cette seule instance. Donc:

>>> my_circle = Circle()
>>> my_circle.radius = 5
>>> my_other_circle = Circle()
>>> print my_other_circle.radius # AttributeError
6
inspectorG4dget

Il existe deux types d'attributs dans Python - Class Data Attributes et Instance Data Attributes.

Python vous donne la flexibilité de créer Data Attributes à la volée.

Puisqu'un attribut de données d'instance est lié à une instance, vous pouvez également le faire dans __init__ méthode ou vous pouvez le faire après avoir créé votre instance ..

class Demo(object):
    classAttr = 30
    def __init__(self):
         self.inInit = 10

demo = Demo()
demo.outInit = 20
Demo.new_class_attr = 45; # You can also create class attribute here.

print demo.classAttr  # Can access it 

del demo.classAttr         # Cannot do this.. Should delete only through class

demo.classAttr = 67  # creates an instance attribute for this instance.
del demo.classAttr   # Now OK.
print Demo.classAttr  

Donc, vous voyez que nous avons créé deux attributs d'instance, un à l'intérieur __init__ et un à l'extérieur, une fois l'instance créée.

Mais une différence est que l'attribut d'instance créé à l'intérieur de __init__ sera défini pour toutes les instances, tandis que s'il est créé à l'extérieur, vous pouvez avoir différents attributs d'instance pour différentes instances.

C'est différent de Java, où chaque instance d'une classe a le même ensemble de variables d'instance.

  • REMARQUE: - Bien que vous puissiez accéder à un attribut de classe via une instance, vous ne pouvez pas le supprimer. De plus, si vous essayez de modifier un attribut de classe via une instance, vous créez en fait un attribut d'instance qui associe l'attribut de classe ..
4
Rohit Jain

Comme l'a dit delnan, vous pouvez obtenir ce comportement avec le __slots__ attribut. Mais le fait qu'il s'agisse d'un moyen d'économiser de l'espace mémoire et du type d'accès ne rejette pas le fait que c'est (aussi) un/le moyen de désactiver les attributs dynamiques.

La désactivation des attributs dynamiques est une chose raisonnable à faire, ne serait-ce que pour éviter les bogues subtils dus à des fautes d'orthographe. "Tester et discipliner" est bien, mais s'appuyer sur une validation automatisée n'est certainement pas faux non plus - et pas nécessairement non-python non plus.

De plus, depuis que la bibliothèque attrs a atteint la version 16 en 2016 (évidemment bien après la question et les réponses d'origine), la création d'une classe fermée avec des emplacements n'a jamais été aussi facile.

>>> import attr
...
... @attr.s(slots=True)
... class Circle:
...   radius = attr.ib()
...
... f = Circle(radius=2)
... f.color = 'red'
AttributeError: 'Circle' object has no attribute 'color'
1
P-Gn