IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Apprendre Python et s'initier à la programmation

Partie 2 : Programmation avancée


précédentsommairesuivant

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).

Image non disponible
Figure 1. Un objet est une instance d'une classe, qui possède son propre état, défini par la valeur de ses différents attributs.

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 (Image non disponible). 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 (Image non disponible). 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.

Image non disponible
Figure 2. Un objet est une instance d'une classe, possède une valeur pour chacun de ses attributs et offre plusieurs fonctionnalités qui y sont décrites.
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

Image non disponible
Figure 3. Un objet est une instance d'une classe, possède une valeur pour chacun de ses attributs et offre plusieurs fonctionnalités qui y sont décrites.

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 :

 
Sélectionnez
1.
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é :

 
Sélectionnez
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 :

 
Sélectionnez
print(brice)
 
Sélectionnez
<__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.

Image non disponible
Figure 4. Un objet de type Contact possède trois attributs représentant respectivement le prénom, le nom et le numéro de téléphone.

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 :

 
Sélectionnez
1.
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 (.) :

 
Sélectionnez
1.
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

Image non disponible
Figure 5. Deux instances d'une même classe occupent deux zones mémoire distinctes déterminant l'identité des objets et dont le contenu définit leur état.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
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 :

 
Sélectionnez
1.
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.

Figure 6. Le fichier classexamples.py contient deux définitions de classe : Vector représente un vecteur dans le plan et Music représente une musique.
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
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 :

 
Sélectionnez
u = Vector(1, -1)
print(u)
 
Sélectionnez
<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 :

 
Sélectionnez
def __str__(self):
    return "(" + str(self.x) + ", " + str(self.y) + ")"

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 :

 
Sélectionnez
(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 :

 
Sélectionnez
1.
2.
3.
4.
5.
def __str__(self):
    result = '"' + self.title + '" par ' + self.artists[0]
    for i in range(1, len(self.artists)):
        result += ', ' + self.artists[i]
    return result

L'exemple suivant et le résultat de son exécution montrent ce qu'on obtient avec cette méthode __str__ :

 
Sélectionnez
m = Music('Si demain', ['Bonnie Tyler', 'Kareen Antonn'], 230)
print(m)
 
Sélectionnez
"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 :

 
Sélectionnez
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 :

 
Sélectionnez
u = Vector(1, -1)
v = Vector(2, 2)
print(u.add(v))

L'exécution de ces trois instructions affiche :

 
Sélectionnez
(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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

Figure 7. On peut utiliser plusieurs opérateurs sur des objets nouvellement définis en les surchargeant dans la classe qui définit ces objets, c'est-à-dire en définissant une méthode avec un nom particulier.

Opérateur

Notation

Méthode à définir

Signe positif

+

__pos__

Signe négatif

-

__neg__

Addition

+

__add__

Soustraction

-

__sub__

Multiplication

*

__mul__

Division

/

__truediv__

Exponentiation

**

__pow__

Division entière

//

__floordiv__

Reste de la division entière

%

__mod__

Égal

==

__eq__

Différent

!=

__ne__

Strictement plus petit

<

__lt__

Plus petit ou égal

<=

__le__

Strictement plus grand

>

__gt__

Plus grand ou égal

>=

__ge__

« non » logique

not

__not__

« et » logique

and

__and__

« ou » logique

or

__or__

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 == :

 
Sélectionnez
1.
2.
3.
4.
5.
u = Vector(1, 1)
v = Vector(1, 1)

print(u == v)
print(u is v)
 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
u.x = 42
 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
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é :

 
Sélectionnez
(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 :

 
Sélectionnez
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
u = Vector(1, 2)
u.__coords = (42, u.__coords[1])
print(u)
 
Sélectionnez
1.
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.

Image non disponible
Figure 8. La classe Vector expose deux propriétés et une méthode publique et possède une variable d'instance privée.
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 :

 
Sélectionnez
1.
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.

Image non disponible
Figure 9. Un carré dans le plan est caractérisé par les coordonnées de son coin inférieur gauche, la longueur de ses côtés et l'angle qu'il forme avec l'horizontale.

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électionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
2.23606797749979

précédentsommairesuivant

Copyright © 2019 Sébastien Combéfis. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.