Maîtriser la Programmation Orientée Objet en Python
Introduction à la POO
Programmation
Python
POO
Auteur·rice
Affiliation
Wilson Toussile
ENSPY & ESSFAR
1 Introduction
Ce tutoriel a pour but de vous introduire aux concepts fondamentaux de la Programmation Orientée Objet (POO) en utilisant le langage Python.
Public Cible : Débutants en programmation ou développeurs avec une connaissance de base de Python (variables, boucles, fonctions).
Objectifs : À la fin de ce tutoriel, vous serez capable de :
Comprendre les concepts fondamentaux de la POO : classes, objets, attributs, méthodes.
Créer et utiliser des classes et des objets en Python.
Appliquer les principes d’encapsulation, d’héritage et de polymorphisme.
Utiliser les méthodes spéciales (comme __init__ et __str__) pour enrichir vos classes.
2 Partie 1 : Les Fondations - Classes et Objets
2.1 Introduction : Pourquoi la Programmation Orientée Objet ?
La programmation orientée objet (POO) est un paradigme de programmation qui s’appuie sur le concept de “classes” et d’“objets”. Elle permet de structurer un programme logiciel en morceaux simples et réutilisables de plans de code (les classes), qui sont utilisés pour créer des instances individuelles d’objets.
Organisation : Regrouper les données (attributs) et les comportements (méthodes) qui leur sont liés.
Réutilisabilité : Utiliser une même classe pour créer de multiples objets.
Maintenance : Modifier une classe en un seul endroit et voir les changements se répercuter sur toutes ses instances.
2.2 Les Classes : Les Plans de Construction
Une classe est un plan (un “blueprint”) pour créer des objets. Un objet est une instance d’une classe.
Exemple 1 (Ma première classe vide)
# Le plan pour créer des objets 'Chien'1class Chien:2pass# Création d'un objet (une instance) à partir de la classe3mon_premier_chien = Chien()4print(type(mon_premier_chien))
1
Déclaration de la classe Chien.
2
pass signifie que la classe ne déclare rien.
3
Instanciation d’un objet de type Chien.
4
Vérification du type de l’objet.
<class '__main__.Chien'>
2.3 Le Constructeur - Les Méthodes - Les Attributs
Le Constructeur - Les Méthodes
Définition 1 (Le Constructeur) Le constructeur est une méthode spéciale, nommée __init__, qui est automatiquement appelée lors de la création d’une nouvelle instance de la classe. Il est utilisé pour initialiser les attributs de l’objet.
Définition 2 (Les Méthodes : Les Actions d’un Objet) Une méthode est une fonction qui est définie à l’intérieur d’une classe. Elle représente une action ou un comportement que les objets (instances) de cette classe peuvent effectuer.
Elle prend toujours en premier paramètre self, qui représente l’instance (objet) courant, lui donnant accès à ses attributs et autres méthodes.
Exemple 2 (Illustration du Constructeur)
1class Chien:2def__init__(self):3print('Instanciation d\'un objet de la classe Chien')4return
1
Déclaration de la classe Chien.
2
Définition du constructeur: notez l’indentation par rapport à class et la présence de self comme premier argument.
3
Affichage d’un message à chaque instanciation d’un objet de type Chien.
4
Tout simple pour marquer la fin de la méthode.
pipo = Chien()chipo = Chien()
Instanciation d'un objet de la classe Chien
Instanciation d'un objet de la classe Chien
Remarques
Affichage du message définit dans le constructeur à chaque instanciation.
Le constructeur est automatiquement exécuté à l’appelle de Chien() où Chien est le nom de la classe en question.
Le nom du constructeur est toujours __init__, entouré de deux _: c’est le cas de toutes les méthodes spéciales prédéfinies en Python, que l’utilisateur redéfinir.
Dans l’exemple Exemple 1, bien que nous n’ayons pas défini le constructeur, il est prédéfini automatiquement. Que nous le redéfinissons, on dit que nous le surchargeons.
L’utilisateur n’a pas à renseigner l’argument self; cet argument ne sert que lors de l’implémentation de la classe.
2.4 Les Attributs : Les Caractéristiques d’un Objet
Les attributs sont les variables qui appartiennent à un objet. Ils sont destinés à contenir des informations spécifiques à l’objet. On les initialise généralement dans le constructeur __init__.
Le constructeur est appelé à la création de l’objet
3
Copie du nom dans l’attribut nom
4
Copie de la race dans l’attribut race
1mon_chien = Chien("Médo", "Labrador")2ton_chien = Chien("Rex", "Berger Allemand")3print(f"Mon chien s'appelle {mon_chien.nom}.")4print(f"Ton chien est un {ton_chien.race}.")5mon_chien.nom ="Médor"6print(f"Mon chien s'appelle désormais {mon_chien.nom}.")
1
Instanciation d’un objet de la classe Chien et affecté à la variable mon_chien.
2
Instanciation d’un autre objet de la classe Chienet affecté à la variable ton_chien.
3
Accès à l’attribut nom de l’objet mon_chien.
4
Accès à l’attribut race de l’objet ton_chien.
5
Modification de l’attribut nom de l’objet mon_chien.
6
Accès à l’attribut nom de l’objet mon_chien après modification.
Mon chien s'appelle Médo.
Ton chien est un Berger Allemand.
Mon chien s'appelle désormais Médor.
Note
En général, on fait en sorte que les valeurs initialles des attributs soient renseignées lors de l’instanciation d’un objet. Dans botre exemple ci-dessus, il s’agit de nom et race.
Remarquez que la manupulation (aussi bien en lecture que en écriture) d’un attribut dans la classe se fait en écrivant self.nom_attribut.
Exemple 4 (Avec des méthodes et attributs)
1class Chien:2def__init__(self, nom, race):self.nom = nomself.race = racereturn3def__str__(self):returnf"{self.nom} est un {self.race}"4def aboyer(self):returnf"{self.nom} dit : Ouaf ! Ouaf !"#----5mon_chien = Chien("Médor", "Labrador")6print(mon_chien)7print(mon_chien.nom)8print(mon_chien.aboyer())
1
Déclaration de la classe Chien.
2
Le constructeur est appelé à la création de l’objet, avec les valeurs des attributs nom et race.
3
__str__ est une méthode spéciale appelée par la fonction print pour obtenir la chaîne de caractères à afficher. Lorsqu’elle n’est pas implémentée, elle affiche la référence en mémoire de l’objet en question.
4
aboyer est une méthode simple.
5
Instanciation d’un objet de la classe Chien et affecté à la variable mon_chien.
6
print affiche la chaîne de caractère obtenu de la méthode spéciale __str__.
7
Accès à l’attribut nom de l’objet mon_chien: notez la différence entre accéder à un attribut et une méthode.
8
Appel de la méthode aboyer.
Médor est un Labrador
Médor
Médor dit : Ouaf ! Ouaf !
Exercice 1 (Classe Voiture) Dans cet exercice, vous allez appliquer les concepts de base de la POO que nous venons de voir : la création de classes, l’initialisation d’attributs via le constructeur __init__, et la définition de méthodes.
Définissez une classe Voiture :
Cette classe doit avoir un constructeur __init__ qui prend en paramètres marque, modele, et annee.
Ces paramètres doivent être utilisés pour initialiser les attributs correspondants de l’objet (self.marque, self.modele, self.annee).
Ajoutez une méthode afficher_details :
Cette méthode ne prendra que self comme argument.
Elle devra afficher les détails de la voiture dans un format lisible, par exemple : "Ma voiture est une [marque] [modele] de [annee].".
Ajoutez une méthode spéciale __str__ :
Implémentez la méthode __str__ pour que, lorsque vous utilisez print() sur une instance de Voiture, elle retourne une chaîne de caractères similaire à celle de afficher_details, mais sans l’afficher directement.
Créez des instances et testez :
Créez au moins deux instances différentes de la classe Voiture avec des informations variées.
Appelez la méthode afficher_details() pour chaque instance.
Utilisez print() directement sur une des instances pour vérifier le fonctionnement de __str__.
3 Partie 2 : Encapsulation et Méthodes Spéciales
3.1 L’Encapsulation : Protéger ses Données
L’encapsulation est le principe dde cacher certains détails d’implémentation d’une classe. Son rôle est de contrôler l’accès aux attributs et méthodes d’une classe, de sorte à maintenir la cohérence des objets.
En Python, on utilise une convention : un attribut ou une méthode préfixé par deux underscores (__) est considéré comme “privé” et ne devrait pas être modifié directement de l’extérieur de la classe.
Exemple 5 (Classe CompteBancaire)
1class CompteBancaire:2def__init__(self, titulaire, solde_initial=0):self.titulaire = titulaire# On préfixe par '_' pour indiquer que cet attribut est "privé"self.__solde = solde_initialreturn3def__str__(self):returnf"Compte de {self.titulaire} avec un solde de {self.__solde}€"4def get_solde(self):# Getter : permet un accès en lecture contrôléreturnself.__solde5def deposer(self, montant):# Méthode pour modifier le solde de manière contrôléeif montant >0:self.__solde += montantprint(f"Dépôt de {montant}€ effectué. Nouveau solde : {self.__solde}€")else:print("Le montant du dépôt doit être positif.")return6def retirer(self, montant):if montant >0:ifself.__solde >= montant:self.__solde -= montantprint(f"Retrait de {montant}€ effectué. Nouveau solde : {self.__solde}€")else:print("Solde insuffisant pour le retrait.")else:print("Le montant du retrait doit être positif.")return#---- 7mon_compte = CompteBancaire("Alice")8print(mon_compte)9mon_compte.deposer(100)10print(mon_compte.get_solde())11mon_compte.retirer(50)12print(mon_compte)13print(f"Solde actuel : {mon_compte.get_solde()}€")
1
Déclaration de la classe CompteBancaire.
2
Définition du constructeur prenant le solde initial, avec une valeur par défaut de 0.
3
Définition de la chaîne de caractères retournée par __str__, pour la fonction print.
4
La méthode get_solde renvoie le solde actuel du compte.
5
La méthode deposer sert à faire un dépôt lorsque la somme est positive.
6
La méthode retirer sert à faire un retrait lorsque la somme est positive et que le solde est suffisant.
7
Instanciation d’un objet de la classe CompteBancaire et affectation à la variable mon_compte.
8
Afficher les informations actuelles du compte.
9
Effectuer un dépôt de 100€.
10
Afficher le solde actuel.
11
Effectuer un retrait de 50€.
12
Afficher les informations actuelles du compte.
13
Afficher le solde actuel.
Compte de Alice avec un solde de 0€
Dépôt de 100€ effectué. Nouveau solde : 100€
100
Retrait de 50€ effectué. Nouveau solde : 50€
Compte de Alice avec un solde de 50€
Solde actuel : 50€
Remarques
L’attribut __solde n’est pas accessible directement depuis l’extérieur de la classe.
Les méthodes deposer et retirer permettent un accès contrôler à __solde, garantissant ainsi la cohérence des données.
Exercice 2 (CEncapsulation : Classe Livre) Dans cet exercice, vous allez mettre en pratique le concept d’encapsulation pour protéger les données internes d’un objet et contrôler leur accès depuis l’extérieur.
Définissez une classe Livre :
Cette classe doit avoir un constructeur __init__ qui prend en paramètres titre, auteur et nombre_de_pages.
Ces paramètres doivent être utilisés pour initialiser les attributs correspondants de l’objet (self.titre, self.auteur).
L’attribut nombre_de_pages doit être considéré comme “privé” et doit donc être préfixé par deux underscores (__nombre_de_pages).
Ajoutez un attribut est_ouvert initialisé à False.
Ajoutez des méthodes pour interagir avec le livre :
ouvrir() : Définit est_ouvert à True si le livre n’est pas déjà ouvert et affiche un message indiquant que le livre est ouvert. Sinon, affiche un message indiquant que le livre est déjà ouvert.
fermer() : Définit est_ouvert à False si le livre est ouvert et affiche un message indiquant que le livre est fermé. Sinon, affiche un message indiquant que le livre est déjà fermé.
lire(pages) : Simule la lecture d’un certain nombre de pages.
Cette méthode ne prendra que self et pages comme arguments.
Si le livre n’est pas ouvert, affichez un message indiquant qu’il faut d’abord ouvrir le livre.
Si pages est supérieur à __nombre_de_pages, affichez un message indiquant que vous avez lu tout le livre. Définissez __nombre_de_pages à 0.
Sinon, diminuez __nombre_de_pages du nombre de pages lues et affichez le nombre de pages restantes.
obtenir_nombre_de_pages() : Une méthode getter pour accéder à l’attribut __nombre_de_pages.
Créez des instances et testez :
Créez au moins deux instances différentes de la classe Livre avec des informations variées.
Essayez d’accéder directement à l’attribut __nombre_de_pages depuis l’extérieur de la classe (cela devrait générer une erreur ou un comportement inattendu).
Utilisez les méthodes ouvrir(), fermer(), lire() et obtenir_nombre_de_pages() pour interagir avec les objets et observer leur comportement.
4 Partie 3 : Héritage et Polymorphisme
4.1 L’Héritage : Ne Vous Répétez Pas (DRY)
L’héritage permet à une classe (dite “enfant” ou “sous-classe”) d’hériter des attributs et méthodes d’une autre classe (dite “parente” ou “super-classe”).
Déclaration de la classe enfant Chien qui hérite de Animal.
2
Le constructeur de la classe enfant prend le nom et la race du chien.
3
Appel du constructeur de la classe parente avec super().
4
Stocker la race dans l’attribut race.
5
Redéfinition de la méthode manger pour un comportement spécifique.
6
Instanciation d’un objet de la classe Chien et affectation à la variable medor.
7
Appel de la méthode manger sur l’objet medor.
Médor dévore sa gamelle.
:::
Exercice 4 (Modélisation de Véhicules) Dans cet exercice, vous allez créer une hiérarchie de classes pour modéliser différents types de véhicules, en utilisant l’héritage pour partager et étendre les fonctionnalités.
Créez la classe parente Vehicule :
Le constructeur __init__ doit prendre marque et annee comme arguments et les stocker comme attributs.
Créez une méthode decrire() qui retourne une chaîne de caractères comme : "Véhicule de marque [marque], année [annee].".
Créez la classe enfant Voiture :
Voiture doit hériter de Vehicule.
Son constructeur __init__ doit accepter marque, annee, et nombre_portes.
Utilisez super().__init__(...) pour appeler le constructeur de la classe parente et initialiser marque et annee.
Initialisez l’attribut spécifique nombre_portes.
Surchargez la méthode decrire dans Voiture :
La nouvelle méthode decrire() doit réutiliser le résultat de la méthode parente et y ajouter les informations spécifiques à la voiture.
Elle doit retourner une chaîne comme : "Voiture : [description du parent], avec [nombre_portes] portes.".
Indice : Appelez super().decrire() à l’intérieur de cette méthode.
Créez une classe “petite-fille” VoitureElectrique :
VoitureElectrique doit hériter de Voiture.
Son constructeur doit accepter marque, annee, nombre_portes, et autonomie_km.
Utilisez super() pour appeler le constructeur de Voiture.
Surchargez à nouveau la méthode decrire() pour inclure l’autonomie, par exemple : "[description de la voiture], avec une autonomie de [autonomie_km] km.".
Testez votre hiérarchie :
Créez une instance de Voiture et une instance de VoitureElectrique.
Appelez la méthode decrire() sur chaque instance et affichez le résultat pour vérifier que l’héritage et la surcharge fonctionnent correctement.
4.3 L’Héritage Multiple
Python, contrairement à d’autres langages comme Java, autorise l’héritage multiple. Cela signifie qu’une classe enfant peut hériter de plusieurs classes parentes simultanément, combinant ainsi leurs attributs et méthodes.
Exemple 6 (Héritage multiple)
1class Pere:def__init__(self, nom_pere):self.nom_pere = nom_perereturndef travailler(self):returnf"{self.nom_pere} va au travail."2class Mere:def__init__(self, nom_mere):self.nom_mere = nom_merereturndef cuisiner(self):returnf"{self.nom_mere} prépare le dîner."# La classe Enfant hérite de Pere ET de Mere3class Enfant(Pere, Mere):def__init__(self, nom_enfant, nom_pere, nom_mere):4 Pere.__init__(self, nom_pere)5 Mere.__init__(self, nom_mere)self.nom_enfant = nom_enfantreturndef jouer(self):returnf"{self.nom_enfant} joue avec ses jouets."# L'instance 'junior' a accès aux méthodes de ses deux parentsjunior = Enfant("Junior", "Jean", "Marie")6print(junior.travailler())7print(junior.cuisiner())8print(junior.jouer())
1
Définition de la classe Pere.
2
Définition de la classe Mere.
3
Définition de la classe Enfant qui hérite de Pere et Mere.
4
Appel du constructeur de Pere.
5
Appel du constructeur de Mere.
6
Appel de la méthode travailler de la classe Pere.
7
Appel de la méthode cuisiner de la classe Mere.
8
Appel de la méthode jouer de la classe Enfant.
Jean va au travail.
Marie prépare le dîner.
Junior joue avec ses jouets.
4.4 Le Problème du Diamant et l’Ordre de Résolution des Méthodes (MRO)
L’héritage multiple introduit une complexité : que se passe-t-il si deux classes parentes ont une méthode avec le même nom ? C’est ce qu’on appelle le problème du diamant.
Python résout cette ambiguïté grâce à un algorithme déterministe appelé Method Resolution Order (MRO). Le MRO définit l’ordre dans lequel Python parcourt la hiérarchie des classes pour trouver une méthode. On peut inspecter le MRO d’une classe avec l’attribut spécial __mro__.
Exemple 7 (Illustration du MRO)
1class Pere:2def__init__(self, nom, profession):self.nomPere = nomself.profession = professionreturn3def saluer(self):returnf"Salut du Père {self.nomPere}, le {self.profession}"4class Mere:5def__init__(self, nom, passion):self.nomMere = nomself.passion = passionreturn6def saluer(self):returnf"Salut de la Mère {self.nomMere}, passionnée de {self.passion}"# L'ordre dans la déclaration (Pere, Mere) est crucial !7class Enfant(Pere, Mere):8def__init__(self, nom_du_pere, nom_de_la_mere, nom_enfant, profession, passion): Pere.__init__(self, nom_du_pere, profession) Mere.__init__(self, nom_de_la_mere, passion)self.nom = nom_enfantreturn9def saluer(self):# Pour éviter l'ambiguïté, on peut appeler explicitement la méthode d'une super-classereturnf"Salut de l'Enfant {self.nom}. Son père {self.nomPere} est {self.profession} et ma mère {self.nomMere} est passionnée de {self.passion}."#----10enfant = Enfant("Jacques", "Jeanne", "Leo", "ingénieur", "jardinage")11print(enfant.saluer())# On peut toujours accéder aux méthodes des parents si nécessaire, en spécifiant la classe12print(Pere.saluer(enfant))13print(Mere.saluer(enfant))# Affichons le MRO de la classe Enfant pour comprendre l'ordre de rechercheprint("\nOrdre de Résolution des Méthodes (MRO) :") 14for cls in Enfant.__mro__:print(cls.__name__)
1
Déclaration de la classe parente Pere.
2
Le constructeur de la classe parente prend le nom et la profession.
3
Méthode saluer de la classe parente Pere
4
Déclaration de la classe parente Mere.
5
Le constructeur de la classe parente prend le nom et la passion.
6
Méthode saluer de la classe parente Mere
7
Déclaration de la classe enfant Enfant qui hérite de Pere et Mere.
8
Le constructeur de la classe enfant prend le nom du père, de la mère, du nom de l’enfant, la profession tu père et la passion de la mère.
9
Méthode saluer de la classe enfant Enfant.
10
Instanciation d’un objet de la classe Enfant et affectation à la variable enfant.
11
Appel de la méthode saluer sur l’objet enfant.
12
Appel de la méthode saluer de la classe Pere sur l’objet enfant.
13
Appel de la méthode saluer de la classe Mere sur l’objet enfant.
14
Affichage du MRO de la classe Enfant.
Salut de l'Enfant Leo. Son père Jacques est ingénieur et ma mère Jeanne est passionnée de jardinage.
Salut du Père Jacques, le ingénieur
Salut de la Mère Jeanne, passionnée de jardinage
Ordre de Résolution des Méthodes (MRO) :
Enfant
Pere
Mere
object
Attention à la complexité !
L’héritage multiple peut rendre le code très difficile à lire et à maintenir. Il est souvent préférable d’utiliser d’autres patrons de conception comme la composition (où un objet contient des instances d’autres objets) ou les mixins pour ajouter des fonctionnalités spécifiques. Utilisez l’héritage multiple avec parcimonie.
5 Partie 4 : Polymorphisme : Une Interface, Plusieurs Formes
Définition 3 (Le Polymorphisme) Le polymorphisme (du grec “plusieurs formes”) est un principe fondamental de la POO qui permet d’utiliser une interface unique pour des objets de types (classes) différents.
Concrètement, cela signifie
6 Conclusion
La programmation orientée objet est un paradigme puissant pour écrire du code modulaire, réutilisable et facile à maintenir. En maîtrisant les classes, l’héritage et le polymorphisme, vous disposez d’outils solides pour construire des applications complexes et bien structurées en Python.
7 Exercices
7.1 Quiz
Question 1
class Point:passp = Point()print(isinstance(p, Point))
Que va afficher ce code ?
Question 2
class Livre:def__init__(self, titre):self.titre = titrelivre_a = Livre("Le Seigneur des Anneaux")livre_b = livre_alivre_b.titre ="Harry Potter"print(livre_a.titre)
Que va afficher ce code ?
Question 3
En programmation orientée objet en Python, à quoi fait référence le paramètre self dans une méthode ?
Question 4
class Calculatrice:def addition(self, a, b):return a + bcalc = Calculatrice()
Quelle est la manière correcte d’appeler la méthode addition ?
Question 5
Quel est le but principal de préfixer un nom d’attribut avec un simple underscore (ex: self._solde) ?
Question 6
class Personne:def__init__(self, nom):self.nom = nomdef__str__(self):returnf"Personne nommée {self.nom}"def__repr__(self):returnf"Personne('{self.nom}')"p = Personne("Alice")print(p)
Que va afficher ce code ?
Question 7
class Parent:def saluer(self):return"Bonjour du parent"class Enfant(Parent):def crier(self):return"AU SECOURS !"e = Enfant()print(e.saluer())
Que va afficher ce code ?
Question 8
class Figure:def aire(self):return"L'aire n'est pas définie"class Carre(Figure):def__init__(self, cote):self.cote = cotedef aire(self):returnself.cote *self.cotec = Carre(5)print(c.aire())
Que va afficher ce code ?
Question 9
class A:def__init__(self):print("A")class B(A):def__init__(self):super().__init__()print("B")b = B()
En raison de l’Ordre de Résolution des Méthodes (MRO) en Python, que va afficher ce code ?
Question 17
class Pizza:def__init__(self, ingredients):self.ingredients = ingredients@classmethoddef margherita(cls):return cls(['mozzarella', 'tomates'])pizza_margherita = Pizza.margherita()
Quel est le rôle principal de la méthode margherita décorée par @classmethod ?
Question 18
Une Voiture est composée d’un Moteur, de Roues, etc. Le Moteur est un objet complexe avec ses propres attributs et méthodes. Quelle est la meilleure façon de modéliser cette relation “est-composé-de” ?