• Aucun résultat trouvé

Accès et modifications des attributs depuis l’extérieur

Cet exemple illuste la puissance de l’héritage et du polymorphisme et la facilité avec laquelle on les utilise en Python. Pour chaque fruit, on utilise la méthode

.affiche_conseil() définie dans la classe mère sans avoir à la réécrire. Bien sûr cet exemple reste simpliste et n’est qu’une « mise en bouche ». Vous verrez des exemples concrets de la puissance de l’héritage dans le chapitre 20 Fenêtres graphiques et Tkinterainsi que dans les exercices du présent chapitre. Avec le module Tkinter, chaque objet graphique (bouton, zone de texte, etc.) est en fait une classe. On peut ainsi créer de nouvelles classes héritant des classes Tkinter afin de personnaliser chaque objet graphique.

Pour aller plus loin

À ce stade, nous pouvons émettre deux remarques :

L’héritage et le polymorphisme donnent toute la puissance à la POO. Toutefois, concevoir ses classes sur un projet, surtout au début de celui-ci, n’est pas chose aisée. Nous vous conseillons de lire d’autres ressources et de vous entraîner sur un maximum d’exemples. Si vous souhaitez allez plus loin sur la POO, nous vous conseillons de lire des ressources supplémentaires. En langue française, vous trouverez les livres de Gérard Swinnen9, Bob Cordeau et Laurent Pointal10, et Vincent Legoff11.

19.5 Accès et modifications des attributs depuis l’extérieur

19.5.1 Le problème

On a vu jusqu’à maintenant que Python était très permissif concernant le changement de valeur de n’importe quel attribut depuis l’extérieur. On a vu aussi qu’il était même possible de créer de nouveaux attributs depuis l’extérieur ! Dans d’autres langages orientés objet ceci n’est pas considéré comme une bonne pratique. Il est plutôt recommandé de définir une interface, c’est-à-dire tout un jeu de méthodes accédant ou modifiant les attributs. Ainsi, le concepteur de la classe a la garantie que celle-ci est utilisée correctement du « côté client ».

Remarque

Cette stratégie d’utiliser uniquement l’interface de la classe pour accéder aux attributs provient des langages orientés objet comme Java et C++. Les méthodes accédant ou modifiant les attributs s’appellent aussi des getters et setters (en français on dit accesseurs et mutateurs). Un des avantages est qu’il est ainsi possible de vérifier l’intégrité des données grâce à ces méthodes : si par exemple on souhaitait avoir un entier seulement, ou bien une valeur bornée, on peut facilement ajouter des tests dans le setter et renvoyer une erreur à l’utilisateur de la classe s’il n’a pas envoyé le bon type (ou la bonne valeur dans l’intervalle imposé).

Regardons à quoi pourrait ressembler une telle stratégie en Python :

1 class Citron :

2 def __init__ (self , couleur =" jaune ", masse =0): 3 self . couleur = couleur

4 self . masse = masse # masse en g 5

6 def get_couleur ( self ): 7 return self . couleur 8

9 def set_couleur (self , value ): 10 self . couleur = value 11

12 def get_masse ( self ): 13 return self . masse 14

15 def set_masse (self , value ): 16 if value < 0:

17 raise ValueError ("Z' avez déjà vu une masse né gative ???") 18 self . masse = value

19 20 21 if __name__ == " __main__ ": 9. https://inforef.be/swi/python.htm 10.https://perso.limsi.fr/pointal/python:courspython3 11.https://openclassrooms.com/fr/courses/235344-apprenez-a-programmer-en-python

Chapitre 19. Avoir la classe avec les objets 19.5. Accès et modifications des attributs depuis l’extérieur

22 # dé finition de citron1 23 citron1 = Citron ()

24 print ( citron1 . get_couleur (), citron1 . get_masse ()) 25 # on change les attributs de citron1 avec les setters 26 citron1 . set_couleur (" jaune fonc é")

27 citron1 . set_masse (100)

28 print ( citron1 . get_couleur (), citron1 . get_masse ())

Lignes 6 à 10. On définit deux méthodes getters pour accéder à chaque attribut.

Lignes 12 à 18. On définit deux méthodes setters pour modifier chaque attribut. Notez qu’en ligne 16 nous testons si la masse est négative, si tel est le cas nous générons une erreur avec le mot-cléraise (cf. chapitre 21 Remarques complémen-taires). Ceci représente un des avantages des setters : contrôler la validité des attributs (on pourrait aussi vérifier qu’il s’agit d’un entier, etc.).

Lignes 22 à 28. Après instanciation, on affiche la valeur des attributs avec les deux fonctions getters, puis on les modifie avec les setters et on les réaffiche à nouveau.

L’exécution de ce code donnera la sortie suivante :

1 jaune 0

2 jaune fonc é 100

Si on avait miscitron1.set_masse(-100) en ligne 26, la sortie aurait été la suivante :

1 jaune 0

2 Traceback ( most recent call last ):

3 File "./ getter_setter .py", line 26, in <module > 4 citron1 . set_masse ( -100)

5 File "./ getter_setter .py", line 17, in set_masse

6 raise ValueError ("Z' avez déjà vu une masse né gative ???") 7 ValueError : Z' avez déjà vu une masse né gative ???

La fonction interneraise nous a permis de générer une erreur car l’utilisateur de la classe (c’est-à-dire nous dans le programme principal) n’a pas rentré une valeur correcte.

On comprend bien l’utilité d’une stratégie avec des getters et setters dans cet exemple. Toutefois, en Python, on peut très bien accéder et modifier les attributs même si on a des getters et des setters dans la classe. Imaginons la même classeCitron que ci-dessus, mais on utilise le programme principal suivant (notez que nous avons simplement ajouter les lignes 9 à 12 ci-dessous) :

1 if __name__ == " __main__ ": 2 # dé finition de citron1 3 citron1 = Citron ()

4 print ( citron1 . get_couleur (), citron1 . get_masse ()) 5 # on change les attributs de citron1 avec les setters 6 citron1 . set_couleur (" jaune fonc é")

7 citron1 . set_masse (100)

8 print ( citron1 . get_couleur (), citron1 . get_masse ()) 9 # on les rechange sans les setters

10 citron1 . couleur = " pourpre profond " 11 citron1 . masse = -15

12 print ( citron1 . get_couleur (), citron1 . get_masse ())

Cela donnera la sortie suivante :

1 jaune 0

2 jaune fonc é 100 3 pourpre profond -15

Malgré la présence des getters et des setters, nous avons réussi à accéder et à modifier la valeur des attributs. De plus, nous avons pu mettre une valeur aberrante (masse négative) sans que cela ne génère une erreur !

Vous vous posez sans doute la question : mais dans ce cas, quel est l’intérêt de mettre des getters et des setters en Python ? La réponse est très simple : cette stratégie n’est pas une manière « pythonique » d’opérer (voir le chapitre 15 Bonnes pratiques en programmation Pythonpour la définition de « pythonique »). En Python, la lisibilité est la priorité. Souvenez-vous du Zen de Python « Readability counts » (voir le chapitre 15).

De manière générale, une syntaxe avec des getters et setters du côté client surcharge la lecture. Imaginons que l’on ait une instance nomméeobj et que l’on souhaite faire la somme de ses trois attributs x, y et z :

1 # pythonique

2 obj .x + obj .y + obj .z 3

4 # non pythonique

19.5. Accès et modifications des attributs depuis l’extérieur Chapitre 19. Avoir la classe avec les objets

La méthode pythonique est plus « douce » à lire, on parle aussi de syntactic sugar ou littéralement en français « sucre syntaxique». De plus, à l’intérieur de la classe, il faut définir un getter et un setter pour chaque attribut, ce qui multiple les lignes de code.

Très bien. Donc en Python, on n’utilise pas comme dans les autres langages orientés objet les getters et les setters ? Mais, tout de même, cela avait l’air une bonne idée de pouvoir contrôler comment un utilisateur de la classe interagit avec certains attributs (par exemple, rentre-t-il une bonne valeur ?). N’existe-t-il pas un moyen de faire ça en Python ? La réponse est : bien sûr il existe un moyen pythonique, la classeproperty. Nous allons voir cette nouvelle classe dans la prochaine rubrique et nous vous dirons comment opérer systématiquement pour accéder, modifier, voire détruire, chaque attribut d’instance de votre classe.

19.5.2 La solution : la classe property

Dans la rubrique précédente, on vient de voir que les getters et setters traditionnels rencontrés dans d’autres langages orien-tés objet ne représentent pas une pratique pythonique. En Python, pour des raisons de lisibilité, il faudra dans la mesure du pos-sible conserver une syntaxeinstance.attribut pour l’accès aux attributs d’instance, et une syntaxe instance.attribut = nouvelle_valeur pour les modifier.

Toutefois, si on souhaite contrôler l’accès, la modification (voire la destruction) de certains attributs stratégiques, Python met en place une classe nomméeproperty. Celle-ci permet de combiner le maintien de la syntaxe lisible instance.attribut, tout en utilisant en filigrane des fonctions pour accéder, modifier, voire détruire l’attribut (à l’image des getters et setters évo-qués ci-dessus, ainsi que des deleters ou encore destructeurs en français). Pour faire cela, on utilise la fonction Python interne property() qui crée un objet (ou instance) property :

1 attribut = property ( fget = accesseur , fset = mutateur , fdel = destructeur )

Les arguments passés àproperty() sont systématiquement des méthodes dites callback, c’est-à-dire des noms de mé-thodes que l’on a définies précédemment dans notre classe, mais on ne précise ni argument, ni parenthèse, niself (voir le chapitre 20 Fenêtres graphiques et Tkinter). Avec cette ligne de code,attribut est un objet de type property qui fonctionne de la manière suivante à l’extérieur de la classe :

— L’instructioninstance.attribut appellera la méthode .accesseur(). — L’instructioninstance.attribut = valeur appellera la méthode

.mutateur().

— L’instructiondel instance.attribut appellera la méthode .destructeur().

L’objetattribut est de type property, et la vraie valeur de l’attribut est stockée par Python dans une variable d’instance qui s’appellera par exemple_attribut (même nom mais commençant par un underscore unique, envoyant un message à l’utilisateur qu’il s’agit d’une variable associée au comportement interne de la classe).

Comment cela fonctionne-t-il concrètement dans un code ? Regardons cet exemple (nous avons mis desprint() un peu partout pour bien comprendre ce qui se passe) :

1 class Citron :

2 def __init__ (self , masse =0):

3 print ("(2) J' arrive dans le . __init__ ()") 4 self . masse = masse

5

6 def get_masse ( self ):

7 print (" Coucou je suis dans le get ") 8 return self . _masse

9

10 def set_masse (self , valeur ):

11 print (" Coucou je suis dans le set ") 12 if valeur < 0:

13 raise ValueError ("Z' avez déjà vu une masse né gative ?"

14 "C' est nawak ")

15 self . _masse = valeur 16

17 masse = property ( fget = get_masse , fset = set_masse ) 18

19

20 if __name__ == " __main__ ":

21 print ("(1) Je suis dans le programme principal , " 22 "je vais instancier un Citron ")

23 citron = Citron ( masse =100)

24 print ("(3) Je reviens dans le programme principal ")

25 print (" La masse de notre citron est {} g". format ( citron . masse )) 26 # on mange le citron

27 citron . masse = 25

Chapitre 19. Avoir la classe avec les objets 19.5. Accès et modifications des attributs depuis l’extérieur

28 print (" La masse de notre citron est {} g". format ( citron . masse )) 29 print ( citron . __dict__ )

Pour une fois, nous allons commenter les lignes dans le désordre :

Ligne 17. Il s’agit de la commande clé pour mettre en place le système :masse devient ici un objet de type property (si on regarde son contenu avec une syntaxeNomClasse.attribut_property, donc ici Citron.masse, Python nous renverra quelque chose de ce style :<property object at 0x7fd3615aeef8>). Qu’est-ce que cela signifie? Et bien la prochaine fois qu’on voudra accéder au contenu de cet attribut.masse, Python appellera la méthode .get_masse(), et quand on voudra le modifier, Python appellera la méthode.set_masse() (ceci sera valable de l’intérieur ou de l’extérieur de la classe). Comme il n’y a pas de méthode destructeur (passée avec l’argumentfdel), on ne pourra pas détruire cet attribut : un del c.masse conduirait à une erreur de ce type :AttributeError: can't delete attribute.

Ligne 4. Si vous avez bien suivi, cette commandeself.masse = masse dans le constructeur va appeler automatiquement la méthode.set_masse(). Attention, dans cette commande, la variable masse à droite du signe = est une variable locale passée en argument. Par contre,self.masse sera l’objet de type property. Si vous avez bien lu la rubrique Différence entre les attributs de classe et d’instance, l’objetmasse créé en ligne 16 est un attribut de classe, on peut donc y accéder avec une syntaxeself.masse au sein d’une méthode.

Conseil

Notez bien l’utilisation deself.masse dans le constructeur (en ligne 4) plutôt que self._masse. Comme self.masse appelle la méthode .set_masse(), cela permet de contrôler si la valeur est correcte dès l’instanciation. C’est donc une pratique que nous vous recommandons. Si on avait utiliséself._masse, il n’y aurait pas eu d’appel à la fonction mutateur et on aurait pu mettre n’importe quoi, y compris une valeur aberrante, lors de l’instanciation.

Lignes 6 à 15. Dans les méthodes accesseur et mutateur, on utilise la variable

self._masse qui contiendra la vraie valeur de la masse du citron (cela serait vrai pour tout autre objet de type property). Attention

Dans les méthodes accesseur et mutateur il ne faut surtout pas utiliserself.masse à la place de self._masse. Pourquoi? Par exemple, dans l’accesseur, si on metself.masse cela signifie que l’on souhaite accéder à la valeur de l’attribut (comme dans le constructeur !). Ainsi, Python rappellera l’accesseur et retombera surself.masse, ce qui rappellera l’accesseur et ainsi de suite : vous l’aurez compris, cela partira dans une récursion infinie et mènera à une erreur du typeRecursionError: maximum recursion depth exceeded. Cela serait vrai aussi si vous aviez une fonction destructeur, il faudrait utiliser self._masse).

L’exécution de ce code donnera :

1 (1) Dans le programme principal , je vais instancier un Citron 2 (2) J' arrive dans le . __init__ ()

3 Coucou je suis dans le set

4 (3) Je reviens dans le programme principal 5 Coucou je suis dans le get

6 La masse de notre citron est 100 g 7 Coucou je suis dans le set

8 Coucou je suis dans le get

9 La masse de notre citron est 25 g 10 {' _masse ': 25}

Cette exécution montre qu’à chaque appel deself.masse ou citron.masse on va utiliser les méthodes accesseur ou mutateur. La dernière commande qui affiche le contenu de citron.__dict__ montre que la vraie valeur de l’attribut est stockée dans la variable d’instance._masse (instance._masse de l’extérieur et self._masse de l’intérieur).

Pour aller plus loin

Il existe une autre syntaxe considérée comme plus élégante pour mettre en place les objets property. Il s’agit des déco-rateurs@property, @attribut.setter et @attribut.deleter. Toutefois, la notion de décorateur va au-delà du présent ouvrage. Si vous souhaitez plus d’informations, vous pouvez consulter par exemple le site programiz12ou le livre de Vincent Legoff13.

12.https://www.programiz.com/python-programming/property