1. Les concepts de base de l’approche orientée objet
Presque tous les programmes et techniques de programmation que vous avez utilisés jusqu’à présent relèvent du style procédural de programmation, même s’il vous est arrivé d’utilisé certains objets intégrés en vous référant à eux.
Le style procédural de programmation a été l’approche dominante du développement logiciel pendant des décennies d’informatique, et il est toujours utilisé aujourd’hui. De plus, il ne disparaîtra pas à l’avenir, car il fonctionne très bien pour des types de projets spécifiques (généralement, pas très complexes et pas grands, mais il y a beaucoup d’exceptions à cette règle).
L’approche objet est assez jeune (beaucoup plus jeune que l’approche procédurale) et est particulièrement utile lorsqu’elle est appliquée à des projets grands et complexes réalisés par de grandes équipes composées de nombreux développeurs.
Ce type de compréhension de la structure d’un projet facilite de nombreuses tâches importantes, par exemple la division du projet en petites parties indépendantes et le développement indépendant de différents éléments du projet.
Python est un outil universel pour la programmation d’objets et de procédures. Il peut être utilisé dans les deux cas.
De plus, vous pouvez créer de nombreuses applications utiles, même si vous ne savez rien des classes et des objets, mais vous devez garder à l’esprit que certains des problèmes (par exemple, la gestion de l’interface utilisateur graphique) peuvent nécessiter une approche d’objet stricte.
Heureusement, la programmation d’objets est relativement simple.
2. Comparaison des approches procédurale et objet
Dans l’approche procédurale, il est possible de distinguer deux mondes différents et complètement séparés:
|
Les fonctions peuvent utiliser des données, mais pas l’inverse. De plus, les fonctions sont capables d’abuser des données, c’est-à-dire d’utiliser la valeur d’une manière non autorisée (par exemple, lorsque la fonction sinus obtient un solde de compte bancaire comme paramètre).
Nous avons dit dans le passé que les données ne pouvaient pas utiliser de fonctions. Mais est-ce tout à fait vrai? Existe-t-il des types particuliers de données qui peuvent utiliser des fonctions?
Oui, il y a - celles nommées méthodes. Ce sont des fonctions qui sont appelées à l’intérieur des données, pas à côté d’elles. Si vous pouvez voir cette distinction, vous avez fait le premier pas dans la programmation d’objets.
>>> texte = "bonjour le monde !"
>>> texte_en_majuscule = texte.upper()
>>> print(texte_en_majuscule)
BONJOUR LE MONDE !
L’approche objet suggère une façon de penser complètement différente :
Figure 1. Diagramme de classes
|
Objets et classes
Figure 2. Exemple très simple de diagramme de classes
|
3. Hiérarchie des classes
Observons le diagramme de classes suivant :
Tous les véhicules existants (et ceux qui n’existent pas encore) sont liés par une seule caractéristique importante : la capacité de se déplacer. Vous pouvez dire qu’un chien bouge aussi; un chien est-il un véhicule? Non, il ne l’est pas. Nous devons améliorer la définition, c’est-à-dire l’enrichir avec d’autres critères, distinguer les véhicules des autres êtres et créer une connexion plus forte. Prenons en considération les circonstances suivantes : les véhicules sont des entités créées artificiellement utilisées pour le transport, déplacées par les forces de la nature et dirigées (conduites) par les humains.
Sur la base de cette définition, un chien n’est pas un véhicule.
La classe des véhicules est très large. Trop large. Nous devons alors définir des classes plus spécialisées :
|
La hiérarchie croît de haut en bas, comme les racines des arbres :
|
On pourrait probablement trouver et organiser d’autres sous-classes potentielles pour la superclasse Véhicules. Il existe de nombreuses classifications possibles. Nous avons choisi des sous-classes basées sur l’environnement :
-
véhicules terrestres;
-
véhicules nautiques;
-
véhicules aériens;
-
véhicules spatiaux.
Dans cet exemple, la première sous-classe uniquement est à son tour spécialisée.
Les véhicules terrestres peuvent être divisés davantage, selon la méthode avec laquelle ils impactent le sol. Ainsi, nous pouvons énumérer:
-
véhicules à roues;
-
véhicules à chenilles;
-
aéroglisseurs.
Direction des flèches
|
3.1. Travail à faire
Imaginez quelles classes pourraient spécialiser les classes restantes.
4. Qu’est-ce qu’un objet ?
|
|
Par exemple : toute voiture personnelle est un objet appartenant à la classe des véhicules à roues. Cela signifie également que la même voiture appartient à toutes les superclasses de sa classe d’origine; par conséquent, elle fait également partie de la classe des véhicules. Votre chien (ou votre chat) est un objet de la classe des mammifères domestiques, ce qui signifie explicitement qu’il est également inclus dans la classe des animaux.
|
4.1. Que possède un objet ?
Les conventions de programmation objet suppose que chaque objet existant possède :
|
Par convention, chaque fois que vous décrivez un objet, vous devez utiliser :
-
un nom : pour définir le nom de l’objet;
-
un adjectif : pour définir une propriété de l’objet;
-
un verbe : pour définir une action de l’objet.
Une classe est un modèle d’une partie très spécifique de la réalité, reflétant des propriétés et des activités trouvées dans le monde réel. Les objets sont définis selon notre propre expérience. Par exemple, la phrase suivante doit conduire à création d’un modèle de programmation objet :
Max est un gros chat qui dort toute la journée.
-
Nom de l’objet :
Max
-
Classe :
chat
-
Attribut :
taille = gros
-
Méthode :
dormir(toute la journée)
4.2. Travail à faire : Définir un objet
Définissez un objet à partir de la phrase suivante :
Une Ferrari Testarossa rouge roule vite
4.3. L’héritage
L’héritage une relation de spécialisation/généralisation.
|
5. Première classe
|
6. Premier objet
La classe définie devient un outil capable de créer de nouveaux objets. L’outil doit être utilisé explicitement, sur demande, pour créer un objet qui sera affecter une variable pour stocker en mémoire :
class SimpleClasse:
pass
un_objet = SimpleClasse()
6.1. Travail à faire : Objet en mémoire
-
Créer la classe
SimpleClasse
et l’objetun_objet
. -
Afficher l’objet
un_objet
et observer le résultat :
Le message renvoyé par python indique que un_objet
contient une référence à une instance de la classe SimpleClasse
, 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.
6.2. Le constructeur
La création d’un objet de la classe se fait en appelant une méthode spéciale et implicite que l’on nomme constructeur. Cette méthode porte le même nom que la classe et peut être redéfinie dans la classe. Le constructeur sert en particulier à définir l’état initial de l’objet et peut accepter des paramètres. Un constructeur n’est rien d’autre qu’une méthode, sans valeur de retour, qui porte un nom imposé par le langage Python : Cette méthode sera appelée lors de la création de l’objet. Le constructeur peut disposer d’un nombre quelconque de paramètres : Cependant, le premier paramètre d’une méthode doit être Exemple
|
Remarquez que l’attribut taille appartient à l’objet En général, il vaut mieux définir les attributs dans la classe : Exemple
|
6.3. Travail à faire : Créer une classe et des objets
-
Créer la classe
Voiture
qui possède les attributs et le méthodes suivantes :-
Attributs :
-
marque
: Chaine de caractères -
modele
: Chaine de caractères -
couleur
: Chaine de caractères
-
-
Méthodes :
-
Constructeur : initialise les attributs avec ses paramètres
-
roule(vitesse)
: retourne la valeur du paramètrevitesse
qui est une chaine de caractères -
descriptiton()
: retourne une phrase sur le modèle : "la Ferrari Testarossa de couleur rouge roule vite"
-
-
-
Créer un objet
voiture1
qui roule vite et afficher sa description :-
marque
:Ferrari
-
modele
:Testarossa
-
couleur
:rouge
-
-
Créer un objet
voiture2
qui roule lentement et afficher sa description :-
marque
:Ligier
-
modele
:JS50
-
couleur
:bleu
-
6.4. La notion d’encapsulation
Le concept d”encapsulation est un concept très utile de la programmation orienté objets. Il permet en particulier d’éviter une modification par erreur des données d’un objet. En effet, il n’est alors plus possible d’agir directement sur les données d’un objet, il est nécessaire de passer par ses méthodes qui jouent le rôle d’interface obligatoire.
On parle dans ce cas d’attributs privés
La protection des attributs d’une classe est réalisée grâce à l’utilisation d’attributs privées. Pour avoir des attributs privés, leur nom doit débuter par Si le nom d’un attributs ne commence pas par |
class Chat:
def __init__(self,nom):
self.__nom = nom
self.__taille = ""
def dormir(self, duree):
return duree
def qui_je_suis(self):
return self.__nom + " est un " + self.__taille + " chat qui dort " + self.dormir("beaucoup")
chat1 = Chat("Max")
chat1.__taille = "gros"
chat2 = Chat("Minou")
chat2.__taille = "petit"
print(chat1.qui_je_suis())
print(chat2.qui_je_suis())
L’affectation de l’attibut __taille
dans le programme n’a eu aucun effet. Il est protégé contre les modifications directes. On peut ici ajouter un paramètre au constructeur pour initialiser cet attribut lors de la construction de l’objet et/ou ajouter un méthode pour y accéder :
class Chat:
def __init__(self,nom, taille = "petit"):
self.__nom = nom
self.__taille = taille
def dormir(self, duree):
return duree
def qui_je_suis(self):
return self.__nom + " est un " + self.__taille + " chat qui dort " + self.dormir("beaucoup")
def set_taille(self, taille):
self.__taille = taille
chat1 = Chat("Max", "gros")
chat2 = Chat("Minou")
print(chat1.qui_je_suis())
print(chat2.qui_je_suis())
chat1.set_taille("très gros")
print(chat1.qui_je_suis())
Accesseurs et mutateurs
Parmi les différentes méthodes que comporte une classe, on a souvent tendance à distinguer :
On rencontre souvent l’utilisation de noms de la forme |
7. Utilisation des objets
7.1. Travail à faire : Créer une classe et des objets
-
Créer la classe
Complexe
:-
Attributs privés :
-
__partie_reel
: nombre réel -
__partie_imaginaire
: nombre réel
-
-
Méthodes :
-
Constructeur : permet de créer un objet de la classe en initialisant tous les attibuts.
-
get_partie_reel()
: retourne la valeur de l’attribut__partie_reel
-
get_partie_imaginaire()
: retourne la valeur de l’attribut__partie_imaginaire
-
set_partie_reel()
: modifie la valeur de l’attribut__partie_reel
-
set_partie_imaginaire()
: modifie la valeur de l’attribut__partie_imaginaire
-
module()
: retourne la valeur du module du nombre complexe (Calculer le module) -
argument()
: retourne la valeur de l’argument du nombre complexe (Calculer l’argument) -
algebrique()
: retourne la forme algébrique du nombre complexe comme3 + 2i
ou3 - 2i
-
additionner(z)
: retourne la somme du nombre complexe courant et du nombre complexz
-
-
-
créer les objets suivant de la classe
Complexe
:-
z1 = 3 + i
-
z2 = 2 - 4i
-
z3 = z1 + z2
-
z1 = z1 + z2
-
-
Pour chaque objet, afficher la forme algébrique, le module et l’argument
8. Execices autocorrigés
-
___ est utilisé(e) pour créer un objet :
-
Quelle est la sortie de cet extrait de code :
class test:
def __init__(self,a="Hello World"):
self.a=a
def display(self):
print(self.a)
obj=test()
obj.display()
-
Quelle est la sortie de cet extrait de code :
class change:
def __init__(self, x, y, z):
self.a = x + y + z
x = change(1,2,3)
y = x.a
x.a = y+1
print(x.a)
-
Quelle est la sortie de cet extrait de code :
class test:
def __init__(self,a):
self.a=a
def display(self):
print(self.a)
obj=test()
obj.display()
-
Le code suivant est-il correct ou retournera-t-il une erreur ?
class A:
def __init__(self,b):
self.b=b
def display(self):
print(self.b)
obj=A("Hello")
del obj
9. Exploitation des classes
Un programme informatique consomme essentiellement deux ressources : du temps de traitement des processeurs, et de l’espace de stockage des données. En machine, l’espace peut être la mémoire vive volatile ou la mémoire de masse persistante.
Il existe trois stratégies d’allocation de la mémoire :
l’allocation statique,
l’allocation dynamique sur la pile,
l’allocation dynamique sur le tas.
L’allocation statique est principalement mise en place dans les langages de programmation compilés (C, C++, Pascal…).
Les langages interprétés (PHP, Python, Perl…) ne peuvent allouer la mémoire que sur demande, lors de l’exécution du script.
À chacune des stratégies correspond une région mémoire du programme, ou segment. Ces segments sont nommés text (statique), stack (pile) et heap (tas).
https://fr.wikipedia.org/wiki/Allocation_de_m%C3%A9moire
Nous allons nous intéresser ici au mécanisme de pile.
9.1. Qu’est-ce qu’une pile ?
Une pile est une structure développée pour stocker des données d’une manière très spécifique. Imaginez une pile de pièces. Vous n’êtes pas en mesure de placer une pièce ailleurs que sur le dessus de la pile. De même, vous ne pouvez pas retirer une pièce de la pile de n’importe quel endroit autre que le haut de la pile. Si vous voulez obtenir la pièce qui se trouve au fond, vous devez retirer toutes les pièces des niveaux supérieurs.
Le nom alternatif pour une pile (mais uniquement dans la terminologie informatique) est LIFO. C’est une abréviation pour une description très claire du comportement de la pile : Last In - First Out. La pièce qui est arrivée en dernier sur la pile sortira en premier.
Une pile est un objet avec deux opérations élémentaires, appelées conventionnellement push (lorsqu’un nouvel élément est placé en haut) et pop (lorsqu’un élément existant est retiré du haut).
Les piles sont utilisées très souvent dans de nombreux algorithmes classiques, et il est difficile d’imaginer la mise en œuvre de nombreux outils largement utilisés sans l’utilisation de piles.
9.2. L’approche procédurale
Tout d’abord, vous devez décider comment stocker les valeurs qui arriveront sur la pile. La méthode la plus simple et d’utiliser une liste. Supposons que la taille de la pile ne soit en aucune façon limitée et que le dernier élément de la liste stocke l’élément supérieur de la pile.
stack = []
Fonction d’ajout d’un élément sur la pile :
-
Nom de la fonction :
push
-
Paramètre : valeur à ajouter sur la pile
-
Retourne : rien
-
La fonction ajoute la valeur à ajouter sur la pile en fin de liste
Fonction de retrait de l’élément du haut de la pile :
-
Nom de la fonction :
pop
-
Paramètre : aucun
-
Retourne : la valeur de l’élément retiré de la pile
-
La fonction retire l’élément situé en haut de la pile, c’est à dire le dernier élément de la liste
9.2.1. Travail à faire : Implémentation de la pile
-
Implémenter le code nécessaire au fonctionement de la pile selon l’approche procédurale.
-
Ajouter à la pile la valeur
3
, observer le contenu de la pile -
Ajouter à la pile la valeur
2
, observer le contenu de la pile -
Ajouter à la pile la valeur
1
, observer le contenu de la pile -
Retirer les éléments de la pile un par un et observer le résultat entre chaque retrait
Correction
|
9.3. L’approche procédurale vs l’approche orientée objet
L’approche procédurale fonctionne, la pile est entièrement implémentée, et vous pouvez l’utiliser si vous en avez besoin.
Mais plus vous l’utilisez souvent, plus vous allez rencontrer des inconvénients. Par exemple :
-
la variable essentielle (la liste) est très vulnérable. N’importe qui peut la modifier de manière incontrôlable, détruisant la pile. Ce n’est pas forcément de la malveillance, cela peut se produire à la suite d’une négligence, par exemple, lorsque quelqu’un confond des noms de variables; imaginez que vous avez accidentellement écrit quelque chose comme ceci, le fonctionnement de la pile serait complètement désorganisé :
stack[0] = 0
-
Si vous avez besoin de plus d’une pile, vous devrez créer d’autre listes pour le stockage de ces autre piles, et probablement aussi d’autres fonctions
push
etpop
. -
Si vous aeez besoin non seulement de fonctions
push
etpop
, mais aussi de certaines autres commodités. Imaginer ce qui se passerait si vous aviez des dizaines de piles implémentées séparément. Il faudrait implémenter des dizaines de fonctions différentes qui font le même travail !
*L’approche objet fournit des solutions pour chacun des problèmes identifiés ci-dessus :
|
9.4. Approche orientée objet
Nous allons donc créer une classe qui permettra de créer des objets Piles :
class Stack:
pass
-
Le constructeur doit créer la liste qui contient la pile. Cette liste est protégée et n’est pas accessible directement depuis l’exterieur de la classe.
-
Les actions de retrait et d’ajout d’un élément de la pile sont les méthodes
pop
etpush
-
En plus de ses deux méthodes, on ajoutera :
-
une méthode
length
qui retourne le nombre d’élément contenu dans la pile -
une méthode
display
qui retourne le contenu de la pile
-
9.5. Travail à faire : Implémentation de la classe Stack
-
Implémenter la classe
Stack
conformément aux indication données ci-dessus : -
Créer un objet
stack1
puis :-
Ajouter la valeur
1
dans la pilestack1
et afficher sa longueur et son contenu -
Ajouter la valeur
2
dans la pilestack1
et afficher sa longueur et son contenu -
Ajouter la valeur
3
dans la pilestack1
et afficher sa longueur et son contenu -
Retirer un à un les éléments de la pile
stack1
et afficher sa longueur et son contenu à chaque fois
-
Correction
|
9.6. Création de plusieurs piles
Nous pouvons désormais avoir plus d’une pile se comportant de la même manière. Chaque pile aura sa propre copie de données privées, mais utilisera le même ensemble de méthodes.
9.7. Travail à faire : Manipuler deux piles
-
Créer deux piles à partir de la même classe :
stack1
etstack
-
Ajouter la valeur
1
dans la pilestack1
et afficher la longueur et le contenu des deux piles -
Retirer un élément de la pile
stack1
et l’ajouter dans la pilestack2
. Afficher la longueur et le contenu des deux piles
Correction
|
9.8. Spécialisation
Allons maintenant un peu plus loin. Ajoutons une nouvelle classe pour gérer les piles.
Cette nouvelle classe devrait être en mesure d’évaluer la somme de tous les éléments actuellement stockés sur la pile.
Nous pourrions modifier la classe Stack
en lui ajoutant une nouvelle méthode mais il arrive parfois que cette classe soit définie dans une librairie et ne soit donc pas modifiable. Il est aussi possible que cette classe nous satisfasse pleinement et que les nouveaux comportements que nous souhaitons soient limités. Nous allons donc spécialiser la classe Stack
, ce qui revient à construire une sous-classe de la classe Stack
.
La première étape est simple: il suffit de définir une nouvelle sous-classe pointant vers la classe qui sera utilisée comme super-classe :
class Stack:
...
class AddingStack(Stack):
pass
L’héritage s’obtient en indiquant dans les parenthèse le nom de la super-classe : La classe
Toutes les instances de |
9.9. Travail à faire : Implémentation de la sous-classe AddingStack
On souhaite :
-
Une méthode
get_sum
retourne la somme de tous les éléments présents dans la pile -
La méthode
push
est redéfinie pour ajouter le nouvel élément à la somme -
La méthode
pop
est redéfinie pour retrancher l’élément sorti de la pile
-
Modifier le constructeur :
class AddingStack(Stack):
def __init__(self):
Stack.__init__(self)
self.__sum = 0
-
__sum
: attribut privé qui contiendra la somme des éléments de la pile -
Stack._init_(self)
: appel explicite au constructeur de la super-classe pour hériter de toutes ses caractéristiques. Nous possédons désormais un objet de la super-classeStack
qui permet d’accéder à ses attributs et ses méthodes :
Stack.push(val) #Appel de la méthode push() définie dans la classe Stack
Stack.pop() #Appel de la méthode pop() définie dans la classe Stack
-
Redéfinir les méthodes
push
etpop
dans la sous-classeAddingStack
:
class AddingStack(Stack):
def __init__(self):
Stack.__init__(self)
self.__sum = 0
def push(self, val):
pass
def pop(self, val):
pass
-
Implémenter la méthode
get_sum
qui retourne la somme des éléments de la pile : -
Créer un objet
stack1
de la classeAddingStack
puis :-
Ajouter la valeur
10
dans la pilestack1
et afficher sa longueur et son contenu et la somme de ses éléments -
Ajouter la valeur
20
dans la pilestack1
et afficher sa longueur et son contenu et la somme de ses éléments -
Ajouter la valeur
30
dans la pilestack1
et afficher sa longueur et son contenu et la somme de ses éléments -
Retirer un à un les éléments de la pile
stack1
et afficher sa longueur, son contenu et la somme de ses éléments à chaque fois
-
Correction
|
10. Conclusion
A l’issue de ce cours, vous devez être familiarisé avec le vocabulaire et les principaux concept de la programmation orienté objet :
-
Classe
-
Objet
-
Attributs ou propriétés
-
Privé/Public
-
Méthodes
-
Constructeur
-
Héritage
-
Super-classe et sous-classe
Je vous invite fortement à faire les exercices suivant :