III. Classe▲
Dans le chapitre précédent, on a vu le principe de la programmation orientée objet et comment il est possible de créer et utiliser des objets dans un programme Python. Ce chapitre présente les classes, construction permettant de définir des nouveaux objets.
III-A. Objet et classe▲
Pour pouvoir créer des objets dans un programme, il faut avant tout avoir défini une classe. Il s'agit en quelque sorte d'un plan, qui décrit les caractéristiques de l'objet. En particulier, une classe définit les deux éléments constitutifs des objets que sont ses attributs et fonctionnalités.
Un objet est donc construit à partir d'une classe et on dit qu'il en est une instance. La classe est le modèle à partir duquel il est possible de créer autant d'objets que l'on souhaite. La figure 1 illustre ce concept en montrant quatre instances qu'il est possible d'obtenir à partir d'une classe GSM (représentée au centre).
III-A-1. Attribut et fonctionnalité▲
Pour rappel, un objet possède des attributs et offre des fonctionnalités. Un attribut est une donnée stockée dans l'objet et qui permet de le caractériser. L'ensemble des valeurs de tous les attributs d'un objet donné définit son état. Une fonctionnalité permet d'effectuer une opération grâce à l'objet. On peut, par exemple, l'interroger pour obtenir une information le concernant, ou alors effectuer une action sur cet objet.
La figure 2 montre une variable g de type GSM qui contient une référence vers un objet, lien indiqué par une flèche simple (). L'objet ainsi créé est une instance d'une classe, représentée à droite sur l'image, lien indiqué par la flèche inversée (). Dans la classe, on peut voir deux parties séparées par un trait horizontal :
- la partie supérieure liste tous les attributs qu'auront tous les objets instanciés à partir de cette classe ;
- la partie inférieure liste toutes les fonctionnalités applicables aux objets qui seront construits à partir de cette classe.
Enfin, dans l'objet, on retrouve les valeurs de tous les attributs. Ainsi, cette instance en particulier a « Sony Ericsson » comme valeur pour l'attribut Marque, « w200i » pour l'attribut Modèle et enfin « Orange-Blanc » comme Couleur.
III-A-1-a. Définition de classe▲
On définit une nouvelle classe à l'aide du mot réservé class suivi d'un nom, d'un deux-points (:) et on termine avec le corps de la classe indenté d'un niveau. L'exemple suivant définit une nouvelle classe appelée Contact qui permet de représenter un élément d'un carnet de contacts :
class
Contact:
pass
Le mot réservé pass représente l'instruction vide, c'est-à-dire que son exécution ne fait rien. On vient donc de définir une classe Contact dont le corps est vide, c'est-à-dire qu'elle ne possède aucun attribut ni fonctionnalité, propres à elle.
On peut dorénavant créer des instances à partir de cette classe. L'exemple suivant crée deux objets à partir de la classe :
a =
Contact
(
)
b =
Contact
(
)
La figure 3 montre la situation en mémoire après exécution de ces deux instructions. On y voit clairement qu'il y a une seule classe Contact à partir de laquelle deux objets ont été créés. De plus, des références vers ces objets se trouvent respectivement dans les variables a et b, qui sont dès lors de type Contact.
III-B. Constructeur et variable d'instance▲
On a vu, dans le chapitre précédent, que le constructeur est appelé lors de la création d'un nouvel objet. Il s'agit en fait d'une méthode particulière dont le code est exécuté lorsqu'une classe est instanciée.
III-B-1. Définition de constructeur▲
Le constructeur se définit dans une classe comme une fonction avec deux particularités :
- le nom de la fonction doit être
__init__
; - la fonction doit accepter au moins un paramètre, dont le nom doit être self, et qui doit être le premier paramètre.
Le paramètre self représente en fait l'objet cible, c'est-à-dire que c'est une variable qui contient une référence vers l'objet qui est en cours de création. Grâce à ce dernier, on va pouvoir accéder aux attributs et fonctionnalités de l'objet cible.
Modifions la classe Contact en lui ajoutant un constructeur qui permet de créer un nouveau contact en renseignant son prénom, son nom et son numéro de téléphone :
2.
3.
4.
5.
6.
7.
class
Contact:
def
__init__
(
self, firstname, lastname, phonenumber):
print
(
firstname)
print
(
lastname)
print
(
phonenumber)
brice =
Contact
(
'Brice'
, 'Thomas'
, 6942
)
Le constructeur de la classe Contact reçoit donc trois paramètres, en plus du paramètre spécial self, et se contente de les afficher. Lorsqu'on crée une instance de cette classe, il faut donc spécifier des valeurs pour les trois paramètres comme on peut le voir sur la dernière instruction qui crée un objet représentant Brice Thomas.
On peut voir ce qui suit affiché à l'écran après exécution du programme, ce qui témoigne bien du fait que le constructeur a été exécuté :
Brice
Thomas
6942
III-B-1-a. Référence▲
Qu'en est-il de la variable brice ? Cette dernière contient tout simplement une référence vers l'objet qui a été créé, c'est-à-dire une indication sur son emplacement en mémoire. On peut observer cela en affichant à l'écran la variable brice, ce qui révèle qu'elle contient une référence vers un objet Contact se trouvant en mémoire à la position 0x10f805c88
:
print
(
brice)
<
__main__.Contact object at 0x10f805c88
>
III-B-1-b. Variable d'instance▲
La classe Contact, telle qu'elle est pour le moment, n'est pas encore des plus utiles. En effet, il serait souhaitable de pouvoir stocker les informations du contact dans l'objet qui a été créé. On voudrait avoir une situation en mémoire telle que celle présentée à la figure 4, c'est-à-dire avoir des attributs stockant les informations du contact.
Un attribut est représenté par une variable particulière, appelée variable d'instance, accessible à l'aide du paramètre self. L'idée est que le constructeur initialise ces variables d'instances, qui seront alors stockées dans l'objet et en mémoire pour toute la durée de vie de l'objet. On modifie donc le constructeur de la classe Contact en :
2.
3.
4.
5.
class
Contact:
def
__init__
(
self, firstname, lastname, phonenumber):
self.firstname =
firstname
self.lastname =
lastname
self.phonenumber =
phonenumber
Il est important de faire la différence entre les deux types de variables qui se trouvent dans le code de ce constructeur :
- la variable self.firstname représente la variable d'instance, c'est-à-dire celle associée à l'objet, qui existe à partir de la création de l'objet jusque sa destruction ;
- la variable firstname représente le paramètre reçu par le constructeur et n'existe que dans le corps de ce dernier.
Le paramètre self permet donc d'accéder aux variables d'instance, c'est-à-dire aux attributs de l'objet cible, depuis le constructeur.
Revenons maintenant à la création d'un objet de type Contact. Puisqu'on a initialisé des variables d'instance dans le constructeur, on va pouvoir y accéder pour n'importe quel objet créé. Si on reprend brice, on va pouvoir afficher la valeur des trois variables d'instance à l'aide de l'opérateur d'accès (.) :
2.
3.
4.
5.
brice =
Contact
(
'Brice'
, 'Thomas'
, 6942
)
print
(
brice.firstname)
print
(
brice.lastname)
print
(
brice.phonenumber)
Comme on peut le constater sur le résultat de l'exécution, les valeurs passées en paramètres au constructeur lors de la création de l'objet ont bien été enregistrées dans les variables d'instance de l'objet :
Brice
Thomas
6942
Les variables d'instance sont spécifiques à chaque objet et ce sont d'ailleurs elles qui définissent l'état de l'objet. Pour illustrer cela, voyons un dernier exemple où on crée deux instances de la classe Contact :
barack =
Contact
(
'Barack'
, 'Obama'
, 9381
)
lurkin =
Contact
(
'Quentin'
, 'Lurkin'
, 0
)
La figure 5 montre l'état de la mémoire après exécution de ces deux instructions. On y voit clairement la classe Contact et les deux instances de cette dernière qui ont été créées. Chaque instance possède bel et bien son espace propre en mémoire, où sont stockées les valeurs de ses variables d'instance.
III-B-1-c. Plusieurs constructeurs▲
Comme on peut le constater, on a spécifié la valeur kitxmlcodeinlinelatexdvp0finkitxmlcodeinlinelatexdvp lorsqu'on a créé l'objet représentant Quentin Lurkin. Cela peut, par exemple, signifier que le numéro de téléphone de ce contact n'est pas connu. Il serait dès lors pratique de pouvoir avoir un deuxième constructeur à qui on ne donnerait que le prénom et le nom.
On ne peut pas avoir plusieurs constructeurs dans une classe, mais on peut utiliser la valeur par défaut des paramètres pour arriver à la même fin. Il suffit donc de modifier le constructeur de la classe Contact comme indiqué ci-dessous :
2.
3.
4.
5.
class
Contact:
def
__init__
(
self, firstname, lastname, phonenumber=
0
):
self.firstname =
firstname
self.lastname =
lastname
self.phonenumber =
phonenumber
On peut maintenant créer Quentin Lurkin sans spécifier de numéro de téléphone et réécrire l'exemple précédent comme suit :
barack =
Contact
(
'Barack'
, 'Obama'
, 9381
)
lurkin =
Contact
(
'Quentin'
, 'Lurkin'
)
Pour résumer cette section, le constructeur est donc une méthode particulière qui est exécutée au moment de la création d'un nouvel objet. Son but principal consiste à initialiser l'objet, c'est-à-dire au moins initialiser ses différentes variables d'instance.
III-C. Méthode▲
Maintenant qu'on est capables d'initialiser un objet et ses attributs, on va pouvoir s'intéresser aux fonctionnalités qui sont, pour rappel, représentées par des méthodes. Une méthode n'est rien de plus qu'une fonction qui s'applique sur un objet cible, spécifié lors de son appel.
III-C-1. Définition de méthode▲
Une méthode se définit dans une classe comme une fonction, avec comme particularité qu'elle doit accepter au moins un paramètre, dont le nom doit être self, et qui doit être le premier paramètre. Ce paramètre représente l'objet cible sur lequel la méthode est appelée. Il permet notamment d'avoir accès aux variables d'instance de l'objet.
Ajoutons, par exemple, une méthode setphonenumber à la classe Contact, qui permet de modifier le numéro de téléphone d'un contact :
2.
3.
4.
5.
6.
7.
8.
class
Contact:
def
__init__
(
self, firstname, lastname, phonenumber=
0
):
self.firstname =
firstname
self.lastname =
lastname
self.phonenumber =
phonenumber
def
setphonenumber
(
self, phonenumber):
self.phonenumber =
phonenumber
La méthode setphonenumber reçoit simplement un numéro de téléphone en paramètre et écrase la valeur de la variable d'instance phonenumber avec cette nouvelle valeur reçue.
III-C-1-a. Appel de méthode▲
Pour appeler une méthode, il faut indiquer l'objet cible suivi de la méthode à appeler en utilisant l'opérateur d'appel (.). L'exemple suivant crée un objet de la classe Contact, sans préciser le numéro de téléphone, puis modifie ce dernier en appelant la méthode setphonenumber :
2.
3.
4.
5.
lurkin =
Contact
(
'Quentin'
, 'Lurkin'
)
print
(
lurkin.phonenumber)
lurkin.setphonenumber
(
8293
)
print
(
lurkin.phonenumber)
La première instruction print
affiche kitxmlcodeinlinelatexdvp0finkitxmlcodeinlinelatexdvp, c'est-à-dire la valeur à laquelle est initialisée la variable d'instance phonenumber par défaut. La seconde instruction print
affiche kitxmlcodeinlinelatexdvp8293finkitxmlcodeinlinelatexdvp, témoignant du fait que l'appel à la méthode setphonenumber a bien fait son travail :
0
8293
III-C-1-b. Plusieurs méthodes de même nom▲
Comme pour les constructeurs, on peut avoir plusieurs méthodes qui ont le même nom en utilisant les valeurs par défaut des paramètres. Définissons, par exemple, une méthode changename qui permet de changer le prénom et/ou le nom d'un contact :
2.
3.
4.
5.
def
changename
(
self, firstname=
None
, lastname=
None
):
if
firstname is
not
None
:
self.firstname =
firstname
if
lastname is
not
None
:
self.lastname =
lastname
On utilise la valeur spéciale None comme valeur par défaut pour les deux paramètres. Il s'agit d'une valeur spéciale dont la signification est justement l'absence de valeur. Dans le corps de la méthode, on modifie les variables d'instance pour le prénom et/ou le nom, si la valeur des paramètres reçus est différente de None. Voici différents appels que l'on peut faire sur un contact :
- lurkin.
changename
(
'John'
,'Doe'
) change le prénom et le nom du contact ; - lurkin.
changename
(
'John'
) change uniquement le prénom du contact ; - lurkin.
changename
(
lastname=
'Doe'
) change uniquement le nom du contact.
III-D. Programmation orientée objet▲
Comme on l'a déjà vu dans le chapitre précédent, en programmation orientée objet, on modélise les objets réels par des objets informatiques. Ces derniers sont l'élément constitutif de ce type de programmation. Cette section présente quelques aspects clés de la programmation orientée objet et la manière de les implémenter en Python.
Voyons avant tout deux nouveaux exemples de classe, présentés au listing de la figure 6. La classe Vector permet de représenter un vecteur dans le plan, composé de deux coordonnées kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp. Elle propose une méthode norm permettant de calculer la norme du vecteur, qui vaut, pour rappel, kitxmlcodeinlinelatexdvp\sqrt{x^2 + y^2}finkitxmlcodeinlinelatexdvp. La classe Music représente une musique décrite par un titre, une liste d'artistes et une durée en secondes. Elle propose une méthode hasAuthor qui teste si un artiste spécifié fait partie des artistes de la musique en question.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
from
math import
sqrt
class
Vector:
def
__init__
(
self, x, y):
self.x =
x
self.y =
y
def
norm
(
self):
return
sqrt
(
self.x **
2
+
self.y **
2
)
class
Music:
def
__init__
(
self, title, artists, duration):
self.title =
title
self.artists =
artists
self.duration =
duration
def
hasAuthor
(
self, name):
return
name in
self.artists
Une fois définies, on peut utiliser ces classes pour en créer des instances. On peut, par exemple, écrire les instructions suivantes :
2.
3.
4.
5.
6.
7.
from
classexamples import
Vector, Music
u =
Vector
(
1
, -
1
)
print
(
u.norm
(
))
m =
Music
(
'Easy Come Easy Go'
, ['Alice on the roof'
], 213
)
print
(
m.hasAuthor
(
'Stromae'
))
On suppose que ces instructions sont placées dans un fichier se trouvant dans le même dossier que le fichier classexamples.py, permettant ainsi l'importation des deux classes depuis le module classexamples.
L'exécution de ces instructions affiche donc d'abord la norme du vecteur kitxmlcodeinlinelatexdvp\overrightarrow{u} = (1, -1)finkitxmlcodeinlinelatexdvp qui vaut kitxmlcodeinlinelatexdvp\sqrt{2} \approx 1.4142...finkitxmlcodeinlinelatexdvp et teste ensuite si Stromae est un artiste de la musique Easy Come Easy Go ou non, et comme ce n'est pas le cas, affiche False
:
1.4142135623730951
False
III-D-1. Représentation d'un objet▲
Comme on l'a vu précédemment, si on tente d'afficher un objet à l'aide de la fonction print
, le résultat montrera le type d'objet ainsi que l'adresse mémoire où il se situe, comme on le voit sur le résultat de l'exécution des instructions suivantes :
u =
Vector
(
1
, -
1
)
print
(
u)
<
classexamples.Vector object at 0x102898b00
>
Ce comportement par défaut n'est pas forcément des plus utiles. Ce qui serait plus intéressant serait de pouvoir obtenir une représentation de l'objet sous forme d'une chaine de caractères. Cette dernière pourrait contenir l'état ou une partie de l'état de l'objet.
En Python, il suffit d'ajouter une méthode nommée __str__
sans paramètre (à part l'obligatoire self puisque c'est une méthode) qui renvoie une chaine de caractères. Voici, par exemple, ce que pourrait être cette méthode pour la classe Vector :
On construit donc une chaine de caractères par concaténation, qui contient les valeurs des coordonnées kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp séparées par une virgule et le tout entouré de parenthèses. Si on exécute de nouveau l'exemple précédent, on obtient maintenant :
(
1
, -
1
)
L'idée derrière la méthode __str__
est donc de proposer une représentation textuelle de l'objet. De manière brute, on pourrait se contenter d'inclure les valeurs de toutes les variables d'instance, mais il est évidemment préférable de construire une représentation qui fait sens, par rapport à l'objet représenté.
Par exemple, pour un objet Music, on peut se limiter à inclure le titre de la musique et la liste des artistes, sans utiliser la durée. On obtiendrait ainsi la méthode suivante :
L'exemple suivant et le résultat de son exécution montrent ce qu'on obtient avec cette méthode __str__
:
m =
Music
(
'Si demain'
, ['Bonnie Tyler'
, 'Kareen Antonn'
], 230
)
print
(
m)
"Si demain"
par Bonnie Tyler, Kareen Antonn
III-D-1-a. Surcharge d'opérateur▲
Revenons maintenant sur la classe Vector. Une opération que l'on doit pouvoir faire consiste à additionner deux vecteurs entre eux. Le résultat de l'opération est un nouveau vecteur dont les composantes sont les sommes des composantes des vecteurs additionnés. Ajoutons pour cela une méthode add dans la classe Vector. Elle prend un vecteur en paramètre, l'additionne avec celui représenté par l'objet cible et renvoie un nouveau vecteur correspondant au résultat de cette somme :
def
add
(
self, other):
return
Vector
(
self.x +
other.x, self.y +
other.y)
La méthode renvoie donc un nouvel objet de type Vector dont les composantes kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp s'obtiennent en faisant la somme des composantes de l'objet représenté par self (le vecteur de l'objet cible) avec celles de l'objet représenté par other (le vecteur reçu en paramètre).
Une fois cette méthode ajoutée à la classe, on peut l'utiliser pour additionner deux vecteurs et, par exemple, écrire :
u =
Vector
(
1
, -
1
)
v =
Vector
(
2
, 2
)
print
(
u.add
(
v))
L'exécution de ces trois instructions affiche :
(
3
, 1
)
Un nouvel objet Vector a donc bien été créé, correspondant à la somme des vecteurs kitxmlcodeinlinelatexdvp\overrightarrow{u} = (1, -1)finkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvp\overrightarrow{v} = (2, 2)finkitxmlcodeinlinelatexdvp. Comme l'opération que l'on a définie représente une addition, ce serait bien de pouvoir utiliser l'opérateur d'addition (+). En Python, il suffit de définir une méthode __add__
qui accepte un paramètre qui est le vecteur à additionner. On remplace donc simplement la méthode add précédemment définie par :
def
__add__
(
self, other):
return
Vector
(
self.x +
other.x, self.y +
other.y)
Grâce à cela, on peut maintenant utiliser directement l'opérateur d'addition pour sommer deux vecteurs :
u =
Vector
(
1
, -
1
)
v =
Vector
(
2
, 2
)
print
(
u +
v)
Cette capacité du langage Python s'appelle la surcharge d'opérateur. La figure 7 reprend les différents opérateurs qu'il est possible de surcharger, avec le nom de la méthode à utiliser.
Opérateur |
Notation |
Méthode à définir |
---|---|---|
Signe positif |
|
|
Signe négatif |
|
|
Addition |
|
|
Soustraction |
|
|
Multiplication |
|
|
Division |
|
|
Exponentiation |
|
|
Division entière |
|
|
Reste de la division entière |
|
|
Égal |
|
|
Différent |
|
|
Strictement plus petit |
|
|
Plus petit ou égal |
|
|
Strictement plus grand |
|
|
Plus grand ou égal |
|
|
« non » logique |
|
__not__ |
« et » logique |
|
__and__ |
« ou » logique |
|
|
III-D-1-b. Égalité▲
On a précédemment vu qu'on pouvait vouloir comparer les états ou les identités de deux objets. La comparaison des états se fait avec l'opérateur d'égalité (==) et celle des identités avec l'opérateur d'identité (is
).
L'exemple suivant crée deux objets Vector distincts en mémoire, c'est-à-dire qu'ils ont des identités différentes. L'utilisation de is
doit donc renvoyer False
. Néanmoins, les deux objets représentent exactement le même vecteur, à savoir kitxmlcodeinlinelatexdvp(1, 1)finkitxmlcodeinlinelatexdvp. Ils ont donc le même état et l'utilisation de == doit renvoyer True
.
Le code suivant et le résultat de son exécution montrent un souci par rapport aux valeurs attendues pour l'utilisation de is
et == :
2.
3.
4.
5.
u =
Vector
(
1
, 1
)
v =
Vector
(
1
, 1
)
print
(
u ==
v)
print
(
u is
v)
False
False
Que s'est-il passé ? On a défini un nouveau type d'objet, représentant des vecteurs, mais on n'a jamais décrit ce que ça signifie pour deux objets Vector d'être égaux. Par défaut, l'opérateur == se comporte comme l'opérateur is
et compare les identités. Pour définir ce que signifie l'égalité de deux objets, il faut redéfinir l'opérateur == en définissant une méthode __eq__
. Dans notre cas, deux vecteurs sont égaux s'ils ont les mêmes valeurs pour leurs composantes respectives. On ajoute donc la méthode suivante dans la classe Vector :
def
__eq__
(
self, other):
return
self.x ==
other.x and
self.y ==
other.y
Si on exécute de nouveau l'exemple précédent, on obtient cette fois-ci le résultat attendu :
True
False
III-D-1-c. Encapsulation▲
L'implémentation de la classe Vector qu'on a faite précédemment utilise deux variables d'instance pour stocker les composantes kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp. Ce n'est évidemment pas la seule possibilité pour implémenter cette classe. On pourrait par exemple utiliser un tuple de deux éléments :
2.
3.
4.
5.
6.
7.
8.
9.
10.
from
math import
sqrt
class
Vector:
def
__init__
(
self, x, y):
self.coords =
(
x, y)
def
norm
(
self):
return
sqrt
(
self.coords[0
] **
2
+
self.coords[1
] **
2
)
# ...suite de la classe
La méthode norm a dû être changée, par rapport à la précédente version, mais elle calcule toujours la même valeur et est toujours appelée de la même manière.
De plus, on constate qu'il y a une différence à faire entre les attributs d'un objet « vus de l'extérieur » (deux coordonnées kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp) et les variables d'instance qu'il contient (une seule dans ce cas-ci).
Les détails d'implémentation, à savoir utiliser deux nombres ou un tuple en variables d'instance dans cet exemple, sont cachés de « l'extérieur ». Le code qui utilise la classe Vector ne doit donc pas être changé si on décide de changer la manière avec laquelle on stocke les données concernant le vecteur dans l'objet représentant ce dernier. Ce principe fondamental de la programmation orientée objet est appelé encapsulation.
L'encapsulation consiste à cacher les détails d'implémentation d'une classe, et ne pas les dévoiler en dehors de cette dernière. Le code qui utilise une classe doit être le plus indépendant possible de la manière avec laquelle la classe est implémentée. Suivant ce principe, il faut donc éviter, dans la mesure du possible, d'accéder directement aux variables d'instance d'un objet, en dehors du code de la classe. Prenons, par exemple, les instructions suivantes :
u =
Vector
(
1
, 1
)
v =
Vector
(
u.x +
1
, u.y)
On souhaite translater le vecteur kitxmlcodeinlinelatexdvp\overrightarrow{u}finkitxmlcodeinlinelatexdvp d'une unité selon l'axe kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp et stocker le vecteur résultant dans une variable v. Exécuter ce code avec la première version de la classe Vector ne posera aucun problème, mais avec la deuxième version, on fera face à une erreur d'exécution :
2.
3.
4.
Traceback (
most recent call last):
File "program.py"
, line 2
, in
v =
Vector
(
u.x +
1
, u.y +
1
)
AttributeError
: 'Vector'
object has no attribute 'x'
En effet, dans la deuxième version de la classe Vector, on n'a plus de variable d'instance associée à l'attribut « extérieur »x. Il aurait fallu écrire l'instruction suivante :
u =
Vector
(
1
, 1
)
v =
Vector
(
u.coords[0
] +
1
, u.coords[1
])
Les deux versions de la classe Vector violent donc le principe d'encapsulation, car un code utilisant des objets Vector dépend fortement des choix d'implémentation de cette classe et est sensible à des modifications de cette dernière.
III-D-1-c-i. Accesseur▲
Il est recommandé de ne pas accéder directement aux variables d'instance d'un objet et de limiter les interactions avec ce dernier à des appels de méthodes. Une méthode qui renvoie la valeur d'un attribut d'un objet est un accesseur. Le nom donné à la méthode est celui que l'on souhaite pour l'attribut et cette dernière doit être définie avec la décoration property. Une décoration est une information que l'on attache à une méthode et qui se déclare avant sa définition avec une arobase (@).
On peut, par exemple, définir deux accesseurs permettant d'obtenir les coordonnées kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp d'un vecteur. Pour la deuxième implémentation de la classe Vector, on y ajoute les définitions suivantes :
2.
3.
4.
5.
6.
7.
@property
def
x
(
self):
return
self.coords[0
]
@property
def
y
(
self):
return
self.coords[1
]
L'attribut x est donc relié à la valeur self.coords[0
] et l'attribut y à la valeur self.coords[1
]. Une fois ces attributs définis, ils peuvent être utilisés en dehors de la classe, pour accéder à leur valeur :
u =
Vector
(
1
, 2
)
print
(
u.x, u.y, sep=
', '
)
On appelle donc un accesseur comme s'il s'agissait d'une variable d'instance, c'est-à-dire à l'aide de son nom. L'exécution du code affiche bien les valeurs des deux coordonnées du vecteur :
1
, 2
Si on décide de modifier la manière avec laquelle on stocke les coordonnées d'un vecteur, il suffira de changer la définition des deux accesseurs. Le code qui utilise des objets Vector en passant par les accesseurs ne devra pas être modifié, l'encapsulation est respectée.
Un accesseur donne donc accès en lecture à un attribut, comme s'il s'agissait d'une variable d'instance. Il n'est par contre pas possible de modifier la valeur d'un attribut, le compilateur provoquant une erreur d'exécution si vous essayez :
u.x =
42
2.
3.
4.
Traceback (
most recent call last):
File "program.py"
, line 3
, in
u.x =
42
AttributeError
: can't set attribute
III-D-1-c-ii. Mutateur▲
Pour pouvoir modifier la valeur d'un attribut, il va falloir définir un mutateur. Il s'agit de nouveau d'une méthode qui porte le même nom que l'attribut désiré et qu'il faut décorer avec le nom de l'attribut suivi de .setter. La méthode accepte un paramètre qui est la nouvelle valeur que l'on désire pour l'attribut.
Pour autoriser la modification des coordonnées d'un vecteur, on ajoute les deux définitions suivantes à la classe Vector :
2.
3.
4.
5.
6.
7.
@x.setter
def
x
(
self, value):
self.coords =
(
value, self.coords[1
])
@y.setter
def
y
(
self, value):
self.coords =
(
self.coords[0
], y)
Comme les tuples sont non modifiables, on affecte un nouveau tuple à la variable d'instance coords, en modifiant la nouvelle coordonnée kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp ou kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp et en reprenant l'ancienne valeur pour l'autre composante.
Une fois les mutateurs définis, on va pouvoir modifier un attribut, de nouveau comme si c'était une variable d'instance. On peut donc, par exemple, écrire :
u =
Vector
(
1
, 2
)
u.x =
42
print
(
u)
Le résultat de l'exécution de ces trois instructions montre que l'attribut x a bien été modifié :
(
42
, 2
)
Dans une classe, on peut soit uniquement définir un accesseur, soit définir un accesseur et un mutateur, dans lequel cas ce dernier doit être défini après l'accesseur.
III-D-1-c-iii. Variable privée▲
À partir du moment où on a défini des accesseurs et des mutateurs pour les attributs, et pour respecter l'encapsulation, on ne devrait plus accéder directement aux variables d'instance en dehors du code de la classe même. Dans la version actuelle de la classe Vector, c'est toujours possible d'écrire les instructions suivantes :
u =
Vector
(
1
, 2
)
u.coords =
(
42
, u.coords[1
])
print
(
u)
Ce code n'est évidemment pas recommandé puisqu'il viole l'encapsulation. Pour bien faire, il faudrait pouvoir empêcher l'accès direct aux variables d'instance. Une variable d'instance privée ne peut être accédée que depuis le corps de la classe la contenant, à partir de self. Tout autre accès provoque une erreur lors de l'exécution.
Pour rendre une variable d'instance privée, il suffit de préfixer son nom avec __. Modifions la classe Vector en rendant la variable d'instance coords privée :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
from
math import
sqrt
class
Vector:
def
__init__
(
self, x, y):
self.__coords =
(
x, y)
@property
def
x
(
self):
return
self.__coords[0
]
@property
def
y
(
self):
return
self.__coords[1
]
# ...suite de la classe
Si on tente d'accéder à la variable d'instance en dehors de la classe, on aura cette fois-ci une erreur lors de l'exécution, que ce soit un accès en lecture ou en écriture :
u =
Vector
(
1
, 2
)
u.__coords =
(
42
, u.__coords[1
])
print
(
u)
2.
3.
4.
Traceback (
most recent call last):
File "program.py"
, line 4
, in
u.__coords =
(
42
, u.__coords[1
])
AttributeError
: 'Vector'
object has no attribute '__coords'
III-D-1-c-iv. Interface publique▲
On appelle interface publique d'un objet, l'ensemble des fonctionnalités qu'elle expose au public, c'est-à-dire qui sont accessibles à partir d'une variable contenant une référence vers l'objet. Lorsqu'on définit une nouvelle classe, il est important de bien penser cette interface publique. C'est en effet via elle que ses instances seront utilisées et, pour bien faire, elle ne devrait pas être trop souvent modifiée.
La figure 8 résume l'interface exposée par la classe Vector. On y voit tout d'abord la variable d'instance coords qui est privée, ce qu'on note avec le kitxmlcodeinlinelatexdvp-finkitxmlcodeinlinelatexdvp. Viennent ensuite les deux propriétés x et y, publiques comme indiqué par le kitxmlcodeinlinelatexdvp+finkitxmlcodeinlinelatexdvp. Enfin, on a la méthode publique norm
(
) qui permet d'obtenir la norme du vecteur.
III-D-1-d. Composition▲
Terminons ce chapitre sur la définition de classes avec la notion de composition. L'idée derrière cette notion clé de la programmation orientée objet consiste à construire des objets à partir d'autres objets. Partons d'un exemple pour comprendre cette notion. On va définir une classe qui représente un carré dessiné dans le plan :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
class
Square:
def
__init__
(
self, lowerleft, side, angle=
0
):
self.__lowerleft =
lowerleft
self.__side =
side
self.__angle =
angle
@property
def
lowerleft
(
self):
return
self.__lowerleft
# ...suite de la classe
Un carré est caractérisé par la coordonnée dans le plan de son coin inférieur gauche, par la longueur de ses côtés et par l'angle qu'il forme avec l'horizontale comme on peut le voir avec le dessin d'une instance de la classe sur la figure 9.
Au lieu de représenter les coordonnées de son coin inférieur gauche avec deux valeurs kitxmlcodeinlinelatexdvpxfinkitxmlcodeinlinelatexdvp et kitxmlcodeinlinelatexdvpyfinkitxmlcodeinlinelatexdvp, on va plutôt utiliser un objet Vector puisqu'on a déjà défini la classe correspondante. Une instance de la classe Square est donc composée à partir d'une instance de Vector. Voici comment on procède pour construire un objet Square :
s =
Square
(
Vector
(
1
, 2
), 5
)
On voit donc bien que le premier paramètre passé au constructeur de la classe Square est un objet Vector, le deuxième est la longueur des côtés et on a laissé la valeur par défaut pour l'angle.
Cette façon de travailler permet donc de réutiliser du code existant et évite de la duplication de code. De plus, on va pouvoir utiliser automatiquement tout ce qui est défini pour les objets Vector, comme la méthode norm, par exemple. Ainsi, on pourrait sans problème écrire le code suivant :
print
(
s.lowerleft.norm
(
))
On part donc de la variable s qui contient une référence vers un objet Square. Via l'un de ses accesseurs, on obtient une référence vers l'objet Vector qui représente les coordonnées du coin inférieur gauche. Comme c'est une instance de la classe Vector, on peut appeler la méthode norm pour obtenir la norme de ce vecteur et l'afficher pour obtenir :
2.23606797749979