Chapitre 11 : Classes, objets, attributs▲
Les chapitres précédents vous ont déjà mis en contact à plusieurs reprises avec la notion d'objet.
Vous savez donc déjà qu'un objet est une entité que l'on construit par instanciation à partir d'une
classe (c'est-à-dire en quelque sorte une « catégorie » ou un « type » d'objet). Par exemple, on peut
trouver dans la bibliothèque Tkinter, une classe Button() à partir de laquelle on peut créer dans une
fenêtre un nombre quelconque de boutons.
Nous allons à présent examiner comment vous pouvez vous-mêmes définir de nouvelles
classes d'objets. Il s'agit là d'un sujet relativement ardu, mais vous l'aborderez de manière très
progressive, en commençant par définir des classes d'objets très simples, que vous perfectionnerez
ensuite. Attendez-vous cependant à rencontrer des objets de plus en plus complexes par après.
Comme les objets de la vie courante, les objets informatiques peuvent être très simples ou très
compliqués. Ils peuvent être composés de différentes parties, qui soient elles-mêmes des objets,
ceux-ci étant faits à leur tour d'autres objets plus simples, etc.
11.1. Utilité des classes▲
Les classes sont les principaux outils de la programmation orientée objet (Object Oriented
Programming ou OOP). Ce type de programmation permet de structurer les logiciels complexes en
les organisant comme des ensembles d'objets qui interagissent, entre eux et avec le monde extérieur.
Le premier bénéfice de cette approche de la programmation consiste dans le fait que les
différents objets utilisés peuvent être construits indépendamment les uns des autres (par exemple
par des programmeurs différents) sans qu'il n'y ait de risque d'interférence. Ce résultat est obtenu
grâce au concept d'encapsulation : la fonctionnalité interne de l'objet et les variables qu'il utilise
pour effectuer son travail, sont en quelque sorte « enfermés » dans l'objet. Les autres objets et le
monde extérieur ne peuvent y avoir accès qu'à travers des procédures bien définies.
En particulier, l'utilisation de classes dans vos programmes vous permettra - entre autres choses -
d'éviter au maximum l'emploi de variables globales. Vous devez savoir en effet que l'utilisation de
variables globales comporte des risques, surtout dans les programmes volumineux, parce qu'il est
toujours possible que de telles variables soient modifiées ou même redéfinies n'importe où dans le
corps du programme (et ce risque s'aggrave particulièrement si plusieurs programmeurs différents
travaillent sur un même logiciel).
Un second bénéfice résultant de l'utilisation des classes est la possibilité qu'elles offrent de
construire de nouveaux objets à partir d'objets préexistants, et donc de réutiliser des pans entiers
d'une programmation déjà écrite (sans toucher à celle-ci !), pour en tirer une fonctionnalité
nouvelle. Cela est rendu possible grâce aux concepts de dérivation et de polymorphisme.
- La dérivation est le mécanisme qui permet de construire une classe « enfant » au départ d'une
classe « parente ». L'enfant ainsi obtenu hérite toutes les propriétés et toute la fonctionnalité de
son ancêtre, auxquelles on peut ajouter ce que l'on veut.
- Le polymorphisme permet d'attribuer des comportements différents à des objets dérivant les uns des autres, ou au même objet ou en fonction d'un certain contexte.
La programmation orientée objet est optionnelle sous Python. Vous pouvez donc mener à bien de nombreux projets sans l'utiliser, avec des outils plus simples tels que les fonctions. Sachez cependant que les classes constituent des outils pratiques et puissants. Une bonne compréhension des classes vous aidera notamment à maîtriser le domaine des interfaces graphiques (Tkinter, wxPython), et vous préparera efficacement à aborder d'autres langages modernes tels que C++ ou Java.
11.2. Définition d'une classe élémentaire▲
Pour créer une nouvelle classe d'objets Python, on utilise l'instruction class.
Nous allons donc apprendre à utiliser cette instruction, en commençant par définir un type d'objet
très rudimentaire, lequel sera simplement un nouveau type de donnée. Nous avons déjà utilisé
différentes types de données jusqu'à présent, mais c'étaient à chaque fois des types intégrés dans le
langage lui-même. Nous allons maintenant créer un nouveau type composite : le type Point.
Ce type correspondra au concept de point en Mathématique.
Dans un espace à deux dimensions, un point est caractérisé par deux nombres (ses coordonnées
suivant x et y). En notation mathématique, on représente donc un point par ses deux coordonnées x
et y enfermées dans une paire de parenthèses. On parlera par exemple du point (25,17). Une
manière naturelle de représenter un point sous Python serait d'utiliser pour les coordonnées deux
valeurs de type float. Nous voudrions cependant combiner ces deux valeurs dans une seule entité,
ou un seul objet. Pour y arriver, nous allons définir une classe Point() :
>
>
>
class
Point:
"
Définition
d
'
un
point
mathématique
"
Les définitions de classes peuvent être situées n'importe où dans un programme, mais on les placera en général au début (ou bien dans un module à importer). L'exemple ci-dessus est probablement le plus simple qui se puisse concevoir. Une seule ligne nous a suffi pour définir le nouveau type d'objet Point(). Remarquons d'emblée que :
- L'instruction class est un nouvel exemple d'instruction composée. N'oubliez pas le double point
obligatoire à la fin de la ligne, et l'indentation du bloc d'instructions qui suit. Ce bloc doit
contenir au moins une ligne. Dans notre exemple ultra-simplifié, cette ligne n'est rien d'autre
qu'un simple commentaire. (Par convention, si la première ligne suivant l'instruction class est
une chaîne de caractères, celle-ci sera considérée comme un commentaire et incorporée
automatiquement dans un dispositif de documentation des classes qui fait partie intégrante de
Python. Prenez donc l'habitude de toujours placer une chaîne décrivant la classe à cet endroit).
- Rappelez-vous aussi la convention qui consiste à toujours donner aux classes des noms qui commencent par une majuscule. Dans la suite de ce texte, nous respecterons encore une autre convention qui consiste à associer à chaque nom de classe une paire de parenthèses, comme nous le faisons déjà pour les noms de fonctions.
Nous venons de définir une classe Point(). Nous pouvons dès à présent nous en servir pour créer des objets de ce type, par instanciation. Créons par exemple un nouvel objet p947 :
>
>
>
p9 =
Point
()
Après cette instruction, la variable p9 contient la référence d'un nouvel objet Point().
Nous pouvons dire également que p9 est une nouvelle instance de la classe Point().
Attention : comme les fonctions, les classes auxquelles on fait appel dans une instruction
doivent toujours être accompagnées de parenthèses (même si aucun argument n'est transmis). Nous
verrons un peu plus loin que les classes peuvent être appelées avec des arguments.
Remarquez bien cependant que la définition d'une classe ne nécessite pas de parenthèses
(contrairement à ce qui de règle lors de la définition des fonctions), sauf si nous souhaitons que la
classe en cours de définition dérive d'une autre classe préexistante (ceci sera expliqué plus loin).
Nous pouvons dès à présent effectuer quelques manipulations élémentaires avec notre nouvel
objet p9. Exemple :
>
>
>
print
p9.__doc__
Définition d'
un
point
mathématique
(Comme nous vous l'avons expliqué pour les fonctions (voir page 73), les chaînes de documentation de divers objets Python sont associées à l'attribut prédéfini __doc__)
>
>
>
print
p9
<
__main__.Point instance at 0x403e1a8c>
Le message renvoyé par Python indique, comme vous l'aurez certainement bien compris tout de suite, que p9 est une instance de la classe Point(), qui est définie elle-même au niveau principal du programme. Elle est située dans un emplacement bien déterminé de la mémoire vive, dont l'adresse apparaît ici en notation hexadécimale (Veuillez consulter votre cours d'informatique générale si vous souhaitez des explications complémentaires à ce sujet).
47 Sous Python, on peut donc instancier un objet à l'aide d'une simple instruction d'affectation. D'autres langages imposent l'emploi d'une instruction spéciale, souvent appelée new pour bien montrer que l'on crée un nouvel objet à partir d'un moule. Exemple : p9 = new Point()
11.3. Attributs (ou variables) d'instance▲
L'objet que nous venons de créer est une coquille vide. Nous pouvons ajouter des composants à cet objet par simple assignation, en utilisant le système de qualification des noms par points48 :
>
>
>
p9.x =
3
.0
>
>
>
p9.y =
4
.0
Les variables ainsi définies sont des attributs de l'objet p9, ou encore des variables d'instance. Elles sont incorporées, ou plutôt encapsulées dans l'objet. Le diagramme d'état ci-contre montre le résultat de ces affectations : la variable p9 contient la référence indiquant l'emplacement mémoire du nouvel objet, qui contient luimême les deux attributs x et y. |
On peut utiliser les attributs d'un objet dans n'importe quelle expression, comme toutes les variables ordinaires :
>
>
>
print
p9.x
3
.0
>
>
>
print
p9.x*
*
2
+
p9.y*
*
2
25
.0
Du fait de leur encapsulation dans l'objet, les attributs sont des variables distinctes d'autres
variables qui pourraient porter le même nom. Par exemple, l'instruction x = p9.x signifie :
« extraire de l'objet référencé par p9 la valeur de son attribut x, et assigner cette valeur à la variable
x ».
Il n'y a pas de conflit entre la variable x et l'attribut x de l'objet p9. L'objet p9 contient en effet son
propre espace de noms, indépendant de l'espace de nom principal où se trouve la variable x.
Remarque importante :
Nous venons de voir qu'il est très aisé d'ajouter un attribut à un objet en utilisant une simple
instruction d'assignation telle que p9.x = 3.0 On peut se permettre cela sous Python (c'est une
conséquence de l'assignation dynamique des variables), mais cela n'est pas vraiment
recommandable, comme vous le comprendrez plus loin. Nous n'utiliserons donc cette façon de faire
que de manière occasionnelle, et uniquement dans le but de simplifier nos explications concernant
les attributs d'instances.
La bonne manière de procéder sera développée dans le chapitre suivant.
11.4 Passage d'objets comme arguments lors de l'appel d'une fonctio
48 Ce système de notation est similaire à celui que nous utilisons pour désigner les variables d'un module, comme par
exemple math.pi ou string.uppercase. Nous aurons l'occasion d'y revenir plus tard, mais sachez dès à présent que
les modules peuvent en effet contenir des fonctions, mais aussi des classes et des variables. Essayez par exemple :
>>> import string
>>> print string.uppercase
>>> print string.lowercase
>>> print string.hexdigits
11.4. Passage d'objets comme arguments lors de l'appel d'une fonction▲
Les fonctions peuvent utiliser des objets comme paramètres (elles peuvent également fournir un objet comme valeur de retour). Par exemple, vous pouvez définir une fonction telle que celle-ci :
>
>
>
def
affiche_point
(p):
print
"
coord
.
horizontale
=
"
, p.x, "
coord
.
verticale
=
"
, p.y
Le paramètre p utilisé par cette fonction doit être un objet de type Point(), puisque l'instruction qui suit utilise les variables d'instance p.x et p.y. Lorsqu'on appelle cette fonction, il faut donc lui fournir un objet de type Point() comme argument. Essayons avec l'objet p9 :
>
>
>
affiche_point
(p9)
coord. horizontale =
3
.0
coord. verticale =
4
.0
Exercice :
(11) Ecrivez une fonction distance() qui permette de calculer la distance entre deux points.
Cette fonction attendra évidemment deux objets Point() comme arguments.
11.5. Similitude et unicité▲
Dans la langue parlée, les mêmes mots peuvent avoir des significations fort différentes suivant le
contexte dans lequel on les utilise. La conséquence en est que certaines expressions utilisant ces
mots peuvent être comprises de plusieurs manières différentes (expressions ambiguës).
Le mot « même », par exemple, a des significations différentes dans les phrases : « Charles et
moi avons la même voiture » et « Charles et moi avons la même mère ». Dans la première, ce que je
veux dire est que la voiture de Charles et la mienne sont du même modèle. Il s'agit pourtant de deux
voitures distinctes. Dans la seconde, j'indique que la mère de Charles et la mienne constituent en
fait une seule et unique personne.
Lorsque nous traitons d'objets logiciels, nous pouvons rencontrer la même ambiguïté. Par
exemple, si nous parlons de l'égalité de deux objets Point(), cela signifie-t-il que ces deux objets
contiennent les mêmes données (leurs attributs), ou bien cela signifie-t-il que nous parlons de deux
références à un même et unique objet ? Considérez par exemple les instructions suivantes :
>
>
>
p1 =
Point
()
>
>
>
p1.x =
3
>
>
>
p1.y =
4
>
>
>
p2 =
Point
()
>
>
>
p2.x =
3
>
>
>
p2.y =
4
>
>
>
print
(p1 =
=
p2)
0
Ces instructions créent deux objets p1 et p2 qui restent distincts, même s'ils ont des contenus
similaires. La dernière instruction teste l'égalité de ces deux objets (double signe égale), et le
résultat est zéro (ce qui signifie que l'expression entre parenthèses est fausse : il n'y a donc pas
égalité).
On peut confirmer cela d'une autre manière encore :
>
>
>
print
p1
<
__main__.Point instance at 00C2CBEC>
>
>
>
print
p2
<
__main__.Point instance at 00C50F9C>
L'information est claire : les deux variables p1 et p2 référencent bien des objets différents.
Essayons autre chose, à présent :
>
>
>
p2 =
p1
>
>
>
print
(p1 =
=
p2)
1
Par l'instruction p2 = p1, nous assignons le contenu de p1 à p2. Cela signifie que désormais ces deux variables référencent le même objet. Les variables p1 et p2 sont des alias49 l'une de l'autre. Le test d'égalité dans l'instruction suivante renvoie cette fois la valeur 1, ce qui signifie que l'expression entre parenthèses est vraie : p1 et p2 désignent bien toutes deux un seul et unique objet, comme on peut s'en convaincre en essayant encore :
>
>
>
p1.x =
7
>
>
>
print
p2.x
7
>
>
>
print
p1
<
__main__.Point instance at 00C2CBEC>
>
>
>
print
p2
<
__main__.Point instance at 00C2CBEC>
49 Concernant ce phénomène d'aliasing, voir également page 138 : copie d'une liste
11.6. Objets composés d'objets▲
Supposons maintenant que nous voulions définir une classe pour représenter des rectangles. Pour
simplifier, nous allons considérer que ces rectangles seront toujours orientés horizontalement ou
verticalement, et jamais en oblique.
De quelles informations avons-nous besoin pour définir de tels rectangles ?
Il existe plusieurs possibilités. Nous pourrions par exemple spécifier la position du centre du
rectangle (deux coordonnées) et préciser sa taille (largeur et hauteur). Nous pourrions aussi
spécifier les positions du coin supérieur gauche et du coin inférieur droit. Ou encore la position du
coin supérieur gauche et la taille. Admettons ce soit cette dernière méthode qui soit retenue.
Définissons donc notre nouvelle classe :
>
>
>
class
Rectangle:
"
définition
d
'
une
classe
de
rectangles
"
... et servons nous-en tout de suite pour créer une instance :
>
>
>
boite =
Rectangle
()
>
>
>
boite.largeur =
50
.0
>
>
>
boite.hauteur =
35
.0
Nous créons ainsi un nouvel objet Rectangle() et deux attributs. Pour spécifier le coin supérieur gauche, nous allons utiliser une instance de la classe Point() que nous avons définie précédemment. Ainsi nous allons créer un objet à l'intérieur d'un autre objet !
>
>
>
boite.coin =
Point
()
>
>
>
boite.coin.x =
12
.0
>
>
>
boite.coin.y =
27
.0
Pour accéder à un objet qui se trouve à l'intérieur d'un autre objet, on utilise la qualification des
noms hiérarchisée (à l'aide de points) que nous avons déjà rencontrée à plusieurs reprises. Ainsi
l'expression boite.coin.y signifie « Aller à l'objet référencé dans la variable boite. Dans cet objet,
repérer l'attribut coin, puis aller à l'objet référencé dans cet attribut. Une fois cet autre objet trouvé,
sélectionner son attribut y. »
Vous pourrez peut-être mieux vous représenter à l'avenir les objets composites, à l'aide de
diagrammes similaires à celui que nous reproduisons ci-dessous :
Le nom « boîte » se trouve dans l'espace de noms principal. Il référence un autre espace de noms réservé à l'objet correspondant, dans lequel sont mémorisés les noms « largeur », « hauteur » et « coin ». Ceux-ci référencent à leur tour, soit d'autres espaces de noms (cas du nom « coin »), soit des valeurs bien déterminées. Python réserve des espaces de noms différents pour chaque module, chaque classe, chaque instance, chaque fonction. Vous pouvez tirer parti de tous ces espaces bien compartimentés afin de réaliser des programmes robustes, c'est-à-dire des programmes dont les différents composants ne peuvent pas facilement interférer.
11.7. Objets comme valeurs de retour d'une fonction▲
Nous avons vu plus haut que les fonctions peuvent utiliser des objets comme paramètres. Elles peuvent également transmettre une instance comme valeur de retour. Par exemple, la fonction trouveCentre() ci-dessous doit être appelée avec un argument de type Rectangle() et elle renvoie un objet Point(), lequel contiendra les coordonnées du centre du rectangle.
>
>
>
def
trouveCentre
(box):
p =
Point
()
p.x =
box.coin.x +
box.largeur/
2
.0
p.y =
box.coin.y +
box.hauteur/
2
.0
return
p
Pour appeler cette fonction, vous pouvez utiliser l'objet boite comme argument :
>
>
>
centre =
trouveCentre
(boite)
>
>
>
print
centre.x, centre.y
37
.0
44
.5
11.8. Les objets sont modifiables▲
Nous pouvons changer les propriétés d'un objet en assignant de nouvelles valeurs à ses attributs. Par exemple, nous pouvons modifier la taille d'un rectangle (sans modifier sa position), en réassignant ses attributs hauteur et largeur :
>
>
>
boite.hauteur =
boite.hauteur +
20
>
>
>
boite.largeur =
boite.largeur – 5
Nous pouvons faire cela sous Python, parce que dans ce langage les propriétés des objets sont
toujours publiques (du moins dans la version actuelle 2.0). D'autres langages établissent une
distinction nette entre attributs publics (accessibles de l'extérieur de l'objet) et attributs privés (qui
sont accessibles seulement aux algorithmes inclus dans l'objet lui-même).
Comme nous l'avons déjà signalé plus haut (à propos de la définition des attributs par assignation
simple, depuis l'extérieur de l'objet), modifier de cette façon les attributs d'une instance n'est pas
une pratique recommandable, parce qu'elle contredit l'un des objectifs fondamentaux de la
programmation orientée objet, qui vise à établir une séparation stricte entre la fonctionnalité d'un
objet (telle qu'elle a été déclarée au monde extérieur) et la manière dont cette fonctionnalité est
réellement implémentée dans l'objet (et que le monde extérieur n'a pas à connaître).
Plus concrètement, nous devrons veiller désormais à ce que les objets que nous créons ne soient
modifiables en principe que par l'intermédiaire de méthodes mises en place spécifiquement dans ce
but, comme nous allons l'expliquer dans le chapitre suivant.