Vocabulaire de la POO
I. Introduction⚓︎
Le paradigme objet
Il existe différentes manières de voir la programmation, on parle de différents paradigmes de programmation. L'un d'eux est le paradigme objet. Lorsque l'on programme en utilisant ce paradigme on parle de programmation objet ou de programmation orientée objet (abrégé POO, ou OOP en anglais pour « Object-oriented programming »). Nous nous limiterons cette année à une introduction de la programmation objet.
Crédits
L'introduction qui suit est en très grande parti extraite du livre "Informatique" de la collection Fluoresciences de l'éditeur Dunod
Auteurs : Delacroix, Joëlle; Barthélémy, François; Fournier-S'niehotta, Raphaël; Gil-Michalon, Isabelle; Lambert, Amélie; Plateau, Agnès; Rovedakis, Stéphane; Simonot, Marianne; Thion, Virginie; Waymel, Emmanuel. Informatique (Fluoresciences) (French Edition)
Les objets
La programmation orientée objet permet de structurer les logiciels en un assemblage d’entités indépendantes qui interagissent. Ces entités sont appelées des objets.
Prenons l’exemple d’un jeu de type tower defense. Il s’agit d’un jeu vidéo dont l’objectif est d’empêcher des vagues d’ennemis d’atteindre une certaine zone en plaçant sur le terrain des tours défensives. Les tours tirent sur les ennemis à leur portée et se différencient les unes des autres selon des caractéristiques comme la portée de leur attaque, les dégâts causés etc. Chaque ennemi tué rapporte des points qui permettront d’acheter de nouvelles tours ou d’améliorer les tours existantes.
Nous aurons un objet terrain, des objets ennemis et des objets tours. Les trois points à retenir sur les objets sont les suivants :
1. Chaque objet possède ses propres caractéristiques : le coût, les dégâts causés, la portée pour les tours, la résistance, la rapidité, les points de vie pour les ennemis etc.
2. Les objets savent faire des choses. C’est ce que l’on appelle leur comportement : les ennemis savent se déplacer, les tours savent attaquer.
3. Les objets ont des interactions, peuvent communiquer : les ennemis interagissent avec le terrain pour savoir vers où ils peuvent aller, les tours interagissent avec les ennemis en les visant ou bien en leur infligeant des dégâts.
L’élaboration d’un programme objet revient donc à trouver les objets permettant d’organiser efficacement le programme et à définir leurs interactions.
Cette façon de structurer les programmes en entités indépendantes qui interagissent offre de nombreux avantages :
- L’élaboration du logiciel est facilement partageable entre plusieurs programmeurs.
- Un objet créé pour un logiciel pourra facilement être réutilisé dans un autre programme.
- Le logiciel sera plus facile à maintenir et à faire évoluer. On pourra adapter le logiciel à de nouvelles exigences en modifiant uniquement le code des objets concernés par ce changement ou bien en ajoutant de nouveaux objets. Le reste du code n’aura pas besoin d’être modifié.
Les classes
Un programme est constitué d’objets qui interagissent, mais certains objets relèvent d’une même catégorie. Notre jeu vidéo est constitué de centaines d’objets qui se répartissent dans trois catégories possibles : les tours, les terrains et les ennemis.
Relever d’une même catégorie signifie qu’il existe des caractéristiques et des comportements communs. Les caractéristiques des tours sont : les dégâts qu’elles infligent, la portée de leurs armes, leur coût et leur capacité à attaquer. En programmation objet, les catégories s’appellent des classes.
L’une des tours lance des flèches et l’autre des boules de feu. En programmation objet, on définit des classes et on crée des objets, exemplaires de ces classes. Faisons une analogie avec un moulage : une fois le moule (la classe) fabriqué, il sert à créer autant d’exemplaires similaires (les objets). Les objets sont des instances d’une classe.
Pour écrire notre jeu en objet, nous aurions alors :
- à définir trois classes : Tour, Ennemi, Terrain,
- à coder l’interaction avec l’utilisateur
- puis à créer des dizaines et des dizaines d’objets tours, d’objets ennemis et un terrain.
La définition d’une classe permet d’introduire de nouveaux types adaptés à notre application. Les variables pourront recevoir des valeurs de ces nouveaux types et, tout comme une variable qui contient un entier ne peut se voir appliquer que des opérations définies sur les entiers, une variable contenant une valeur instance d’une classe ne pourra se voir appliquer que les comportements prévus dans la classe.
Définitions
En programmation orientée objet, un programme est un ensemble d’entités qui interagissent. Ces entités sont appelées des objets. Un objet possède un état (les données qui le caractérise) et des comportements (ce qu’il sait faire).
Une classe permet de définir une famille d’objets. À partir d’une classe, on peut créer autant d’objets que l’on veut. Ce sont des exemplaires, on dit en fait des instances de la classe.
Le mot clé class⚓︎
Le mot clé class
Pour définir une classe en Python, on utilise le mot-clé class. C’est une nouvelle instruction composée qui doit donc être suivie de « : ». À l’intérieur de la classe (c’est-à-dire dans tout le texte indenté qui suit l’entête avec le mot class), on définit les caractéristiques et les comportements communs aux objets de la classe. Les caractéristiques s’appellent les attributs et les comportements s’appellent les méthodes.
Exemple
À la place de pass viendront les attributs et méthodes de cette classe.
Une définition de classe est une instruction comme une autre en Python. On peut donc la mettre n’importe où. En pratique, on écrit un module par classe et on importe le module. Par convention, le nom d’une classe commence par une majuscule.
Créer des objets à partir d'une classe⚓︎
Des objets à partir d'une classe
Une classe étant un moule, l’importation d’une classe ne produit rien de concret sinon de donner la possibilité de créer des objets instances de cette classe, des ennemis concrets. Ceci se fait de la façon suivante :
Exemple
grosMechant et unAutreEnnemi sont deux ennemis concrets fabriqués à partir du même moule.
Attributs et constructeurs⚓︎
Exemple
Pour l’instant, notre classe est une coquille vide. Il faut maintenant y ajouter les caractéristiques communes aux ennemis. Dans notre version simplifiée, les caractéristiques seront les suivantes :
- leur position sur le terrain repérée par une position en X et une position en Y ;
- leurs points de vie ;
- leur rapidité ;
Vocabulaire
En terminologie objet, il faut définir dans la classe Ennemi quatre attributs ou variables d’instances. Ceci se fait de la façon suivante :
Exemple
__init__
Nous avons déjà rencontré le mot clé def pour définir des fonctions. Une classe est constituée essentiellement d’une suite de définitions de fonctions qui représentent les fonctions que l’on va pouvoir utiliser sur les objets instances de la classe. Les fonctions internes aux classes s’appellent des méthodes.
La méthode __init__ est une méthode particulière qu’on appelle le constructeur de la classe. Cette méthode est utilisée lors de la création des objets. Son paramètre self représente l’objet qui va être crée.
Attention, il y a deux tirets underscore avant et après le mot init .
Attributs
Le corps de __init__ indique qu’à chaque fois que l’on crée un objet ennemi, il faut lui associer quatre variables qui lui sont propres, nommées posX, posY, pv et rapidite et les initialiser avec les valeurs respectives 0, 0, 100 et 2. Tous les ennemis possèderont ces quatre variables. Ces variables s’appellent les attributs ou les variables d’instance de la classe. Ce sont bien des caractéristiques communes aux ennemis. Mais ils possèdent chacun leur propre copie de ces variables : leurs valeurs pourront donc évoluer de façon indépendante.
Accéder aux attributs des objets d'une classe⚓︎
Exemple
Nos deux objets grosMechant et unAutreEnnemi naissent avec les mêmes caractéristiques et les mêmes valeurs pour ces caractéristiques. Nous pouvons accéder aux valeurs de ces attributs pour chacun d’entre eux au moyen de la notation pointée.
Testons
Exécuter le script ci-dessous :
Dans la console située en dessous du script, recopier et exécuter ligne par ligne à la main (sans copier/coller) :
Résumé
grosMechant.pv désigne l’attribut pv de grosMechant, unAutreEnnemi.pv désigne l’attribut pv de unAutreEnnemi. On peut modifier la valeur des points de vie de grosMechant cela n’affecte en rien les points de vie de unAutreEnnemi. Les objets d’une même classe ont donc les mêmes attributs. Les valeurs de ces attributs sont propres à chaque objet et évoluent de façon indépendante au fil de l’exécution du programme : on peut tout à fait imaginer que les points de vie de grosMechant tombent à 0 au bout de trois tours alors qu’unAutreEnnemi survit jusqu’au bout.
Des constructeurs plus souples⚓︎
Des paramètres
-
Avec le constructeur que nous avons écrit, tous les objets de la classe
Ennemisont initialisés à la création avec les mêmes valeurs. Ceci n’est pas très pratique. Nous aimerions pouvoir créer un vrai gros méchant très lent mais avec plein de points de vie et un ennemi moins costaud mais beaucoup plus rapide. En d’autres termes, nous aimerions pouvoir choisir les valeurs des attributsrapiditeetpvau moment de la création (instanciation) d’un objet. -
Comme
__init__est une méthode comme une autre, il suffit de lui ajouter des paramètres permettant de transmettre les valeurs que l’on souhaite pour les attributs dont l’initialisation doit varier. Dans notre cas, nous allons ajouter deux paramètresretppour fixer la valeur de ces deux attributs. Il suffira alors d’initialiserself.rapiditeavec l’une etself.pvavec l’autre. Nous n’ajoutons pas de paramètres permettant de transmettre une valeur pourposXetposYcar nous voulons que tous les ennemis partent du même endroit.
Testons
Nous avons maintenant le droit de transmettre deux valeurs lorsque nous créons des objets Ennemi.
Exécuter le script ci-dessous :
# Tests (insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
Dans la console située en dessous du script, recopier et exécuter ligne par ligne à la main (sans copier/coller) :
👉 Créez votre propre instance de la classe Ennemi_2, et faites afficher ses attributs comme ci-dessus.
Les méthodes⚓︎
Les méthodes
Nous avons dit en introduction que définir une classe c’est définir des attributs et les comportements communs aux objets de la classe. Nous savons maintenant définir les attributs. Qu’en est-il des comportements ?
Les comportements représentent ce que les objets savent faire. Dans notre version, nous voulons doter les ennemis de capacités très rudimentaires : se déplacer vers un nouveau point, recevoir des points de dégâts et afficher leurs caractéristiques. Pour définir un comportement, il suffit d’ajouter une méthode à notre classe. Ces méthodes auront toujours self comme premier paramètre de façon à désigner l’objet sur lequel va s’appliquer la méthode. Lors de l’appel de la méthode, la notation pointée nous permettra de désigner l’objet qui prendra la place de self.
La méthode recevoirDegats
Un ennemi doit pouvoir subir un certain nombre de dégâts. Nous ajoutons donc une méthode recoitDegats dans la classe Ennemi. Cette méthode possède deux paramètres : self, qui désigne l’objet ennemi qui subit les dégâts et deg, le nombre de points de dégâts qu’il doit recevoir. Lorsqu’un ennemi reçoit des points de dégâts, cela doit modifier ses points de vie. Puisque self désigne l’objet sur lequel on applique la méthode, self.pv désigne son attribut personnel pv. Il suffit donc d’enlever la valeur de deg à cet attribut.
Exécuter le script ci-dessous :
# Tests (insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
👉 Jouez un peu avec la méthode recoitDegats
Méthode
Une méthode se définit donc exactement comme une fonction à la seule différence que son premier paramètre est toujours self qui désigne l’objet sur lequel s’appliquera la méthode. Par voie de conséquence, le corps de la méthode peut faire référence à self.
Remarque
🌵 La méthode recoitDegats recoit 2 arguments : self et deg.
Mais dans l'appel on écrit : mechant.recoitDegats(25)
Donc un seul paramètre !!!
self est l'objet lui même, ici nommé mechant.
Les méthodes seDeplace et affiche
Un ennemi doit pouvoir se déplacer vers une nouvelle position. Nous ajoutons donc une méthode seDeplace dans la classe Ennemi_4. Cette méthode possède trois paramètres : self et deux entiers représentant les coordonnées de la nouvelle la nouvelle position. Cela doit modifier les attributs posX et posY. Pour afficher, on se contente d’afficher les valeurs de tous les attributs de l’objet. Seul self est en paramètre.
# Tests (insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
-
Maintenant que notre classe est enrichie de trois comportements, nous pouvons les utiliser sur des objets instances de la classe
Ennemi_4. -
Pour faire se déplacer grosMechant au point d’abscisse 5 et d’ordonnée 8, il suffit d’écrire
grosMechant.seDeplace( 5,8). Le corps de la méthode va être exécuté dans un contexte oùselfdésigneragrosMechant,nouveauXdésignera 5 etnouveauYdésignera 8 et modifiera uniquement les attributsposXetposYdegrosMechant. - Soulignons le fait que si
selfdoit toujours être le premier paramètre d’une méthode, il n’est pas présent entre les parenthèses lors de l’appel de la méthode. Si une méthode dans une classe C est de la formenomMethode( self, par1,.., parn) alors on l’utilise de la façon suivante :unObjet.uneMethode( val1,... valn). - Le corps de la méthode sera alors exécuté avec
unObjetà la place deself,val1à la place depar1, …etvalnà la place deparn.
Dans la console située en dessous du script, recopier et exécuter ligne par ligne à la main (sans copier/coller) :
Méthode et classe
👉 Les méthodes définies dans une classe ne sont applicables que sur les objets instances de la classe. Inversement, un objet instance d’une classe ne peut se voir appliquer que les méthodes de cette classe.
Tester
# Tests (insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
# Tests (insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
Solution
Dans ces exemples, a contient une valeur de type int mais pas de valeur de type Ennemi_4. Les méthodes de Ennemi_4 ne lui sont donc pas applicables. De la même façon, grosMechant contient une valeur de type Ennemi_4 et + n’est pas définie sur les valeurs de ce type.
Les méthodes peuvent renvoyer un résultat⚓︎
Les méthodes peuvent renvoyer un résultat
Les méthodes ont le plus souvent pour rôle de modifier l’état de l’objet sur lequel elles s’appliquent. Elles peuvent aussi, comme les fonctions, renvoyer un résultat. Par exemple, dans notre classe, nous pouvons définir une méthode estVivant qui teste si l’ennemi sur lequel elle s’applique a encore des points de vie.
Testons
Exécuter le script ci-dessous :
# Tests (insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
Dans la console située en dessous du script, recopier et exécuter ligne par ligne à la main (sans copier/coller) :
Des méthodes spéciales⚓︎
La méthode spéciale __str__()
Nous connaissons la méthode print() pour les valeurs de type simple. Cette méthode est aussi applicable sur des objets. Par défaut, elle affiche des informations inexploitables. Vous pouvez choisir ce qu’elle affiche pour les classes que vous définissez en ajoutant dans ces classes la méthode __str__(). La méthode __str__() doit renvoyer une chaîne de caractères. print() utilisera cette chaîne de caractères pour faire l’affichage. Ceci est beaucoup plus souple que de définir des méthodes afficher(). Pour la classe Ennemi, on remplacerait la méthode affiche() par la méthode __str__() définie ainsi :
Testons
Exécuter le script ci-dessous :
# Tests (insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
😊 Nous pouvons à présent afficher un ennemi en utilisant print().
👉 Faites vos propres essais ...
Objets et références⚓︎
Objets et références
Lorsque nous lions une variable à un objet, la variable ne contient pas l’objet lui- même, mais une référence à l’objet, c’est-à-dire l’adresse mémoire où se situe l’objet. Dès lors, deux variables peuvent référencer le même objet. Ce sont alors deux moyens d’accès au même objet. On peut donc modifier l’objet par l’intermédiaire de l’une des variables comme de l’autre. Cette possibilité ouvre la voie à de nombreuses erreurs de programmation. Il faut donc être capable de faire la différence entre deux variables référençant des objets ayant la même valeur et deux variables qui référencent le même objet.
Testons
Exécuter le script ci-dessous :
# Tests (insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
Dans la console située en dessous du script, recopier et exécuter ligne par ligne à la main (sans copier/coller) :
Objets et références
La valeur de la variable x est donnée par Ennemi_6( 10,20). Ceci crée un nouvel objet ennemi en mémoire et x reçoit comme valeur l’adresse de ce nouvel objet qui nous est indiquée.
Il en est de même pour y : il y a création d’un nouvel objet stocké dans une adresse mémoire différente et y reçoit cette adresse comme valeur. En revanche,
z = y donne à z la valeur de y à savoir l’adresse de y
Les variables x et y, bien qu’étant liées à des objets ayant les mêmes valeurs, sont indépendantes : la modification de l’une n’a aucun impact sur l’autre. Ce n’est pas la même chose pour y et z comme l’indique l’exemple suivant :
Testons
Exécuter le script ci-dessous :
# Tests (insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
Objets et références
Le fait d’avoir modifié les points de vie de y a aussi modifié ceux de z.
🌵 Cette notion est rendue confuse du fait de l’abus de langage que nous faisons : nous ne devrions pas dire que nous modifions les points de vie de y mais que nous modifions les points de vie de l’objet référencé par y.
Créer des clones⚓︎
Objets et références
🙃 Le fait que deux variables puissent partager le même objet doit être utilisé à bon escient. Prenons l’exemple d’un jeu contenant des monstres qui ont un pouvoir de clonage. La classe suivante représente ces monstres et contient deux versions possibles de la méthode permettant de créer un clone.
Testons
Exécuter le script ci-dessous :
# Tests (insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)
Résumé
Dans notre exemple, mic a reçu huit points de dégâts et a donc vingt-deux points de vie, alors que clone1 qui a été créé par copie de mic au moment où celui-ci avait vingt-cinq points de vie a reçu vingt-cinq points de dégâts et est donc mort.
La méthode seClonerV2() est tout à fait différente. Au lieu de créer un nouveau monstre ayant les mêmes valeurs que celles du monstre original, elle renvoie self, c’est-à-dire une référence au monstre original. Toute attaque sur le monstre affaiblira son clone et inversement.
Ici, clone2 se retrouve avec sept points de vie : il est affecté par les dégâts qu’il reçoit (15) mais aussi par ceux que moc reçoit (3). Il en est de même pour moc. Clairement, pour créer un clone, c’est la première version qu’il faut choisir.
Remarque
😢 Nous laissons au lecteur le soin d’imaginer le temps que l’on peut mettre à repérer l’erreur lorsque l’on a écrit la seconde version sans en connaître les conséquences...
🐘 Ce qu'il faut retenir⚓︎
À savoir 💚
-
Une classe permet de définir un ensemble d’objets qui ont des caractéristiques communes. C’est un moule permettant de créer une infinité d’objets différents dans leurs valeurs mais similaires dans leur structure. Ces objets sont les instances de la classe
-
Définir une classe c’est définir l’ensemble des attributs et des méthodes caractérisant tous les objets instances de la classe.
-
Les attributs sont en général déclarés privés afin d’en interdire l’accès à l’extérieur de la classe. C’est ce qu’on appelle l’encapsulation.
-
La méthode
__init__est le constructeur de la classe. Elle est utilisée à la création des objets de la classe et initialise les valeurs des attributs de l’objet. -
Pour utiliser une méthode
m(self, …)d’une classeC, il faut avoir un objetmon_objetinstance de la classeCet faire :mon_objet.m(...)
II. Vocabulaire de la programmation objet⚓︎
Les objets
La programmation objet consiste à regrouper données et traitements dans une même structure appelée objet. Elle possède l'avantage de localiser en un même endroit toute l'implémentation d'une structure de données abstraite.
Prenons une voiture comme exemple
Une voiture peut être considérer comme un objet.
-
On peut lui associer des informations comme sa couleur, sa marque et sa catégorie : il s'agit des attributs de notre objet.
-
On peut également définir des mécanismes concernant cet objet comme démarrer, accélérer, freiner, klaxonner : il s'agit des méthodes qui vont s'appliquer sur notre objet.
Les objets
Concrètement, un objet est une structure de données abstraite regroupant :
- des données associées à cet l'objet que l'on appelle des attributs.
- des fonctions (ou procédures) s'appliquant sur l'objet que l'on appelle des méthodes.
Exemple de classe
Reprenons l'exemple de la voiture
Ses attributs ( couleur, marque, ...) et méthodes ( démarrer, accélerer, ...) sont réunis dans ce qu'on appelle une classe qui est donc un modèle (moule) décrivant un objet, ici la voiture. On va donc définir une classe Voiture qui va être le moule (modèle) pour la fabrication de tous les objets voitures. On peut la schématiser ainsi :
classDiagram
class Voiture{
String Couleur
String Marque
String Catégorie
Démarrer()
Accélérer()
Freiner()
Klaxonner()
}
Les objets sont ensuite créés à partir de ce moule. On peut fabriquer deux objets clio et c3, qui sont deux instances de la classe Voiture en écrivant simplement les deux instructions suivantes :
Les instances
On va donc pouvoir créer facilement des objets Voiture grâce à cette classe (moule).
Il suffit pour les construire d'utiliser le nom de la classe (qui est aussi le nom du constructeur d'objets de cette classe). Chaque objet ainsi créé est une instance de la classe.
Remarque
- Les méthodes définies dans une classe ne sont applicables que sur les objets instances de la classe.
- Inversement, un objet instance d’une classe ne peut se voir appliquer que les méthodes de cette classe.
Accès aux attributs et aux méthodes
Pour accéder aux attributs et aux méthodes d'une classe on utilise la notation pointée.
Dans notre exemple :
clio.marquedonne la marque de l'objet clio, c'est à dire Renaultclio.klaxonner()va faire klaxonner notre voiture (virtuelle). Nous reviendrons sur cela un peu plus loin.
III. Classes et objets en Python⚓︎
En Python, tout est objet !
Vous ne le saviez sans doute pas, mais les objets vous connaissez déjà (et oui !)
Les listes en Python
Les listes sont un type abstrait list dans python; et vous utilisez la notation pointée pour ajouter un élément.
Recopier une par une les lignes suivantes, et les exécuter une par une.
Les types en Python
L'affichage montre que tous les types en Python sont des classes. Les entiers sont des objets de la classe int, les flottants sont des objets de la classe float, etc. Pour créer un entier ou une liste il suffit de les construire en utilisant le nom de leurs classes respectives
Recopier une par une les lignes suivantes, et les exécuter une par une.
>>> type(int)
>>> type(float)
>>> entier = int()
>>> type(entier)
>>> ma_liste = list()
>>> type(ma_liste)
Interface d'une classe
En définissant une classe on définit un type abstrait de données. Comme tout type abstrait, une classe possède donc une interface qui décrit l'ensemble des méthodes auxquelles on a accès pour manipuler les objets de cette classe.
On peut utiliser la fonction dir pour lister tous les attributs et méthodes d'un objet.
Taper dir(list) dans la console python pour visualiser les différentes méthodes et attributs de list.
Les méthodes du type prédéfini list
On retrouve ici les méthodes applicables sur les objets du type prédéfini list.
Son interface est disponible dans la documentation officielle
Vous noterez que l'interface ne précise pas la façon dont les méthodes sont implémentées mais juste la façon de les utiliser, ce qui suffit largement généralement.
On constate aussi qu'il y a de nombreuses méthodes dont le nom est encadré d'un double underscore __.
Ce sont des méthodes spéciales . Nous reviendrons sur quelques-unes d'entre elles un peu plus tard.
# Tests(insensible à la casse)(Ctrl+I)
(Alt+: ; Ctrl pour inverser les colonnes)
(Esc)