4.0 Python - Programmation Orientée Objet (POO)
Programmation Orientée Objet (POO) en Python
La Programmation Orientée Objet (POO) est un paradigme de programmation puissant et flexible largement utilisé en développement logiciel moderne. Ce guide vous présente les concepts fondamentaux de la POO en Python. Vous découvrirez comment créer et utiliser des classes, implémenter l'encapsulation pour protéger vos données, exploiter l'héritage pour réutiliser du code, et personnaliser la représentation de vos objets.
Objectifs
✅ Comprendre et expliquer le concept de classes et d'objets en Python
✅ Créer des classes avec des attributs et des méthodes
✅ Utiliser le constructeur __init__ pour initialiser des objets
✅ Différencier les attributs de classe et les attributs d'instance
✅ Appliquer le principe d'encapsulation pour protéger les données d'un objet
✅ Implémenter l'héritage pour créer des hiérarchies de classes
✅ Utiliser la méthode spéciale __str__ pour personnaliser la représentation string d'un objet
✅ Concevoir et implémenter un système simple utilisant les principes de la POO
Sommaire
1. Classes et Objets : Les Piliers de la POO
Qu'est-ce qu'une classe ?
Une classe en Python est comme un plan ou un modèle pour créer des objets. Pensez-y comme à un moule à gâteau : le moule (la classe) définit la forme et la structure, tandis que les gâteaux (les objets) sont les instances réelles créées à partir de ce moule.
Exemple : La Classe Voiture
class Voiture:
def __init__(self, marque, modele, annee):
self.marque = marque
self.modele = modele
self.annee = annee
self.kilometrage = 0
def afficher_info(self):
print(f"{self.annee} {self.marque} {self.modele}, Kilométrage: {self.kilometrage} km")
def conduire(self, distance):
self.kilometrage += distance
print(f"La voiture a parcouru {distance} km. Nouveau kilométrage: {self.kilometrage} km")
# Création d'un objet (instance) de la classe Voiture
ma_voiture = Voiture("Renault", "Clio", 2020)
ma_voiture.afficher_info()
ma_voiture.conduire(100)
ma_voiture.afficher_info()
Explications détaillées :
-
Définition de la classe :
class Voiture:crée une nouvelle classe nommée Voiture. -
Méthode
__init__: C'est le constructeur de la classe. Il est appelé automatiquement lors de la création d'un nouvel objet.selffait référence à l'instance en cours de création.- Les autres paramètres (
marque,modele,annee) sont utilisés pour initialiser les attributs de l'objet.
-
Attributs :
self.marque,self.modele, etc., sont des attributs de l'objet. Ils stockent les données spécifiques à chaque instance. -
Méthodes :
afficher_infoetconduiresont des méthodes. Ce sont des fonctions définies à l'intérieur de la classe qui peuvent interagir avec les attributs de l'objet. -
Création d'un objet :
ma_voiture = Voiture("Renault", "Clio", 2020)crée une nouvelle instance de la classe Voiture. -
Utilisation de l'objet : On peut appeler les méthodes sur l'objet créé, comme
ma_voiture.afficher_info().
2. Méthodes : Le Comportement des Objets
Les méthodes définissent ce qu'un objet peut faire. Elles sont similaires aux fonctions, mais sont liées à une classe spécifique.
Exemple : Compte Bancaire
class CompteBancaire:
def __init__(self, proprietaire, solde_initial=0):
self.proprietaire = proprietaire
self.solde = solde_initial
def deposer(self, montant):
if montant > 0:
self.solde += montant
print(f"Dépôt de {montant}€ effectué. Nouveau solde: {self.solde}€")
else:
print("Le montant du dépôt doit être positif.")
def retirer(self, montant):
if 0 < montant <= self.solde:
self.solde -= montant
print(f"Retrait de {montant}€ effectué. Nouveau solde: {self.solde}€")
else:
print("Montant invalide ou solde insuffisant.")
def afficher_solde(self):
print(f"Solde du compte de {self.proprietaire}: {self.solde}€")
# Utilisation de la classe
compte_alice = CompteBancaire("Alice", 1000)
compte_alice.afficher_solde()
compte_alice.deposer(500)
compte_alice.retirer(200)
compte_alice.afficher_solde()
Explications détaillées :
-
Méthodes avec paramètres :
deposeretretirerprennent un paramètremontanten plus deself. -
Logique interne : Ces méthodes contiennent des vérifications (comme s'assurer que le montant est positif) avant de modifier l'état de l'objet.
-
Modification de l'état : Les méthodes peuvent modifier les attributs de l'objet (
self.solde). -
Retour d'information : Les méthodes peuvent afficher des informations ou renvoyer des valeurs.
3. Encapsulation : Protéger les Données
L'encapsulation est un principe fondamental de la POO qui consiste à regrouper les données et les méthodes qui les manipulent, et à restreindre l'accès direct à certaines composantes.
L'encapsulation en Python est souvent réalisée par convention, en utilisant des noms d'attributs commençant par un underscore pour indiquer qu'ils sont "privés".
1. Concept d'encapsulation et attributs "privés" en Python
En programmation orientée objet, l'encapsulation est le principe de regrouper les données (attributs) et les méthodes qui les manipulent dans une seule unité (la classe), et de contrôler l'accès à ces données. L'idée est de cacher les détails internes de l'objet et de ne fournir qu'une interface contrôlée pour interagir avec lui. En Python, il n'y a pas de véritable "privé" comme dans d'autres langages. Au lieu de cela, Python utilise une convention de nommage :
- Un attribut commençant par un seul underscore (comme
_solde) est considéré comme "privé par convention". Cela signifie que les développeurs ne devraient pas y accéder directement en dehors de la classe, mais techniquement, ils peuvent toujours le faire. - Un attribut commençant par deux underscores (comme
__solde) utilise le "name mangling" de Python, ce qui rend l'accès plus difficile (mais pas impossible) en dehors de la classe.
Exemple : Compte Bancaire Sécurisé
class CompteBancaireSecurise:
def __init__(self, proprietaire, solde_initial=0):
self._proprietaire = proprietaire # Attribut "privé"
self._solde = solde_initial # Attribut "privé"
def deposer(self, montant):
if montant > 0:
self._solde += montant
print(f"Dépôt de {montant}€ effectué.")
else:
print("Le montant du dépôt doit être positif.")
def retirer(self, montant):
if 0 < montant <= self._solde:
self._solde -= montant
print(f"Retrait de {montant}€ effectué.")
else:
print("Montant invalide ou solde insuffisant.")
def obtenir_solde(self):
return self._solde
def definir_solde(self, nouveau_solde):
if nouveau_solde >= 0:
self._solde = nouveau_solde
else:
print("Le solde ne peut pas être négatif.")
# Utilisation
compte = CompteBancaireSecurise("Charlie", 1000)
print(compte.obtenir_solde()) # Utilisation de la méthode pour accéder au solde
compte.definir_solde(1500) # Utilisation de la méthode pour modifier le solde
Explications détaillées :
- Attributs "privés" :
_proprietaireet_soldesont marqués comme "privés" par convention. - Méthodes d'accès :
obtenir_solde()etdefinir_solde()contrôlent l'accès et la modification du solde. - Contrôle des données : La méthode
definir_solde()vérifie que le nouveau solde est valide avant de le modifier.
2. Différence entre les deux exemples
Dans le premier exemple (CompteBancaire):
- Les attributs proprietaire et solde sont directement accessibles.
- N'importe qui peut lire ou modifier ces attributs sans restriction.
compte_alice = CompteBancaire("Alice", 1000)
print(compte_alice.solde) # Accès direct à l'attribut
compte_alice.solde = -500 # Modification directe, potentiellement dangereuse
Dans le second exemple (CompteBancaireSecurise):
- Les attributs
_proprietaireet_soldesont marqués comme "privés" par convention. - Des méthodes spécifiques (
obtenir_soldeetdefinir_solde) sont fournies pour accéder et modifier le solde. - Ces méthodes permettent un contrôle supplémentaire (par exemple, empêcher un solde négatif).
compte = CompteBancaireSecurise("Charlie", 1000)
print(compte.obtenir_solde()) # Accès contrôlé
compte.definir_solde(-500) # Tentative de définir un solde négatif, sera refusée
3. Pourquoi utiliser l'encapsulation ?
L'encapsulation offre plusieurs avantages :
- Contrôle d'accès : Vous pouvez valider les données avant de les modifier.
- Flexibilité : Vous pouvez changer l'implémentation interne sans affecter le code qui utilise la classe.
- Maintien de l'intégrité des données : Vous empêchez les modifications accidentelles ou non autorisées.
4. Exemple amélioré pour mieux illustrer l'encapsulation
Voici une version améliorée qui montre mieux l'intérêt de l'encapsulation :
class CompteBancaireEncapsule:
def __init__(self, proprietaire, solde_initial=0):
self._proprietaire = proprietaire
self._solde = solde_initial
self._transactions = []
def deposer(self, montant):
if montant > 0:
self._solde += montant
self._transactions.append(f"Dépôt: +{montant}€")
print(f"Dépôt de {montant}€ effectué.")
else:
print("Le montant du dépôt doit être positif.")
def retirer(self, montant):
if 0 < montant <= self._solde:
self._solde -= montant
self._transactions.append(f"Retrait: -{montant}€")
print(f"Retrait de {montant}€ effectué.")
else:
print("Montant invalide ou solde insuffisant.")
def obtenir_solde(self):
return self._solde
def obtenir_releve(self):
return self._transactions
# Utilisation
compte = CompteBancaireEncapsule("David", 1000)
compte.deposer(500)
compte.retirer(200)
print(f"Solde actuel: {compte.obtenir_solde()}€")
print("Relevé des transactions:")
for transaction in compte.obtenir_releve():
print(transaction)
Dans cet exemple :
- Les attributs
_soldeet_transactionssont "privés". - L'utilisateur de la classe ne peut pas accéder directement à ces attributs, ce qui empêche des modifications non contrôlées.
- La méthode obtenir_releve fournit un accès en lecture seule à l'historique des transactions.
L'encapsulation ici permet de maintenir l'intégrité des données (solde toujours cohérent avec les transactions) et offre une interface claire pour interagir avec le compte bancaire.
4. Attributs de Classe vs Attributs d'Instance
Les attributs peuvent être liés à la classe elle-même ou à des instances spécifiques de la classe.
Exemple : Gestion des Employés
class Employe:
nombre_employes = 0 # Attribut de classe
def __init__(self, nom, salaire):
self.nom = nom # Attribut d'instance
self.salaire = salaire # Attribut d'instance
Employe.nombre_employes += 1
@classmethod
def afficher_nombre_employes(cls):
print(f"Nombre total d'employés : {cls.nombre_employes}")
def afficher_details(self):
print(f"Nom: {self.nom}, Salaire: {self.salaire}€")
# Utilisation
employe1 = Employe("Alice", 50000)
employe2 = Employe("Bob", 60000)
Employe.afficher_nombre_employes() # Accès à l'attribut de classe
employe1.afficher_details() # Accès aux attributs d'instance
Explications détaillées :
- Attribut de classe :
nombre_employesest partagé par toutes les instances de la classe. - Attributs d'instance :
nometsalairesont spécifiques à chaque instance. - Méthode de classe :
afficher_nombre_employesutilise le décorateur@classmethodet peut accéder aux attributs de classe. - Incrémentation de l'attribut de classe : Chaque fois qu'un nouvel employé est créé,
nombre_employesest incrémenté.
5. Héritage : Étendre les Fonctionnalités
L'héritage permet de créer une nouvelle classe basée sur une classe existante, héritant de ses attributs et méthodes.
Exemple : Hiérarchie des Véhicules
class Vehicule:
def __init__(self, marque, modele):
self.marque = marque
self.modele = modele
def afficher_info(self):
print(f"Véhicule: {self.marque} {self.modele}")
class Voiture(Vehicule):
def __init__(self, marque, modele, nombre_portes):
super().__init__(marque, modele)
self.nombre_portes = nombre_portes
def afficher_info(self):
super().afficher_info()
print(f"Nombre de portes: {self.nombre_portes}")
class Moto(Vehicule):
def __init__(self, marque, modele, type_moto):
super().__init__(marque, modele)
self.type_moto = type_moto
def afficher_info(self):
super().afficher_info()
print(f"Type de moto: {self.type_moto}")
# Utilisation
ma_voiture = Voiture("Renault", "Clio", 5)
ma_moto = Moto("Harley-Davidson", "Sportster", "Cruiser")
ma_voiture.afficher_info()
ma_moto.afficher_info()
Explications détaillées :
- Classe de base :
Vehiculeest la classe parent. - Classes dérivées :
VoitureetMotohéritent deVehicule. - Fonction
super(): Utilisée pour appeler des méthodes de la classe parente. - Surcharge de méthode :
afficher_infoest redéfinie dans les classes enfants pour ajouter des informations spécifiques.
6. La Méthode Spéciale __str__
La méthode __str__ est une méthode spéciale en Python qui permet de définir comment un objet doit être représenté sous forme de chaîne de caractères. Cette méthode est automatiquement appelée lorsque vous utilisez les fonctions print() ou str() sur votre objet.
Exemple : Représentation d'un Livre
class Livre:
def __init__(self, titre, auteur, annee):
self.titre = titre
self.auteur = auteur
self.annee = annee
def __str__(self):
return f"{self.titre} par {self.auteur} ({self.annee})"
# Utilisation
mon_livre = Livre("1984", "George Orwell", 1949)
print(mon_livre) # Appelle automatiquement __str__
Explications détaillées :
-
Définition de
__str__: Cette méthode doit toujours retourner une chaîne de caractères. -
Utilisation automatique : Quand vous utilisez
print(mon_livre), Python appelle automatiquement la méthode__str__de l'objet. -
Personnalisation : Vous pouvez personnaliser la façon dont votre objet est représenté en chaîne, ce qui est très utile pour le débogage et l'affichage.
-
Différence avec
__repr__: Il existe une méthode similaire appelée__repr__qui est plus destinée à une représentation "officielle" de l'objet, souvent utilisée pour le débogage. Si__str__n'est pas défini, Python utilisera__repr__à la place.
Exemple étendu : Classe Personne avec __str__
class Personne:
def __init__(self, nom, age, profession):
self.nom = nom
self.age = age
self.profession = profession
def __str__(self):
return f"{self.nom}, {self.age} ans, {self.profession}"
# Utilisation
p1 = Personne("Alice Dupont", 30, "Ingénieure")
p2 = Personne("Bob Martin", 25, "Designer")
print(p1) # Affiche : Alice Dupont, 30 ans, Ingénieure
print(p2) # Affiche : Bob Martin, 25 ans, Designer
# Utilisation dans une liste
personnes = [p1, p2]
for personne in personnes:
print(personne) # Utilise __str__ pour chaque personne
Dans cet exemple, la méthode __str__ permet d'afficher de manière claire et concise les informations de chaque personne. Cela rend le code plus lisible et facilite l'affichage d'informations pertinentes sur les objets.
Challenge
Créez un système de gestion de bibliothèque en utilisant la POO. Le système doit comprendre les classes suivantes :
Livre: Représente un livre avec des attributs tels que titre, auteur, ISBN, et statut (emprunté ou disponible).Membre: Représente un membre de la bibliothèque avec des attributs comme nom, ID de membre, et liste des livres empruntés.Bibliotheque: Gère la collection de livres et les membres.
Implémentez les fonctionnalités suivantes :
- Ajouter et supprimer des livres de la bibliothèque
- Enregistrer de nouveaux membres
- Permettre aux membres d'emprunter et de retourner des livres
- Afficher la liste des livres disponibles
- Afficher la liste des livres empruntés par un membre
- Rechercher un livre par titre ou auteur
Assurez-vous d'utiliser l'encapsulation appropriée et de gérer les cas d'erreur (par exemple, emprunter un livre déjà emprunté).
Critères de validation
- Le fichier en lien est consultable et en lecture
- Toutes les missions sont codées correctement et testées
Une solution ici