Archives Wild Code School
Python

4.0 Python - Programmation Orientée Objet (POO)

Quête 22 sur 66

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

python
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 :

  1. Définition de la classe : class Voiture: crée une nouvelle classe nommée Voiture.

  2. Méthode __init__ : C'est le constructeur de la classe. Il est appelé automatiquement lors de la création d'un nouvel objet.

    • self fait 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.
  3. Attributs : self.marque, self.modele, etc., sont des attributs de l'objet. Ils stockent les données spécifiques à chaque instance.

  4. Méthodes : afficher_info et conduire sont des méthodes. Ce sont des fonctions définies à l'intérieur de la classe qui peuvent interagir avec les attributs de l'objet.

  5. Création d'un objet : ma_voiture = Voiture("Renault", "Clio", 2020) crée une nouvelle instance de la classe Voiture.

  6. 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

python
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 :

  1. Méthodes avec paramètres : deposer et retirer prennent un paramètre montant en plus de self.

  2. 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.

  3. Modification de l'état : Les méthodes peuvent modifier les attributs de l'objet (self.solde).

  4. 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é

python
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 :

  1. Attributs "privés" : _proprietaire et _solde sont marqués comme "privés" par convention.
  2. Méthodes d'accès : obtenir_solde() et definir_solde() contrôlent l'accès et la modification du solde.
  3. 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.
python
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 _proprietaire et _solde sont marqués comme "privés" par convention.
  • Des méthodes spécifiques (obtenir_solde et definir_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).
python
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 :

  1. Contrôle d'accès : Vous pouvez valider les données avant de les modifier.
  2. Flexibilité : Vous pouvez changer l'implémentation interne sans affecter le code qui utilise la classe.
  3. 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 :

python
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 _solde et _transactions sont "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

python
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 :

  1. Attribut de classe : nombre_employes est partagé par toutes les instances de la classe.
  2. Attributs d'instance : nom et salaire sont spécifiques à chaque instance.
  3. Méthode de classe : afficher_nombre_employes utilise le décorateur @classmethod et peut accéder aux attributs de classe.
  4. Incrémentation de l'attribut de classe : Chaque fois qu'un nouvel employé est créé, nombre_employes est 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

python
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 :

  1. Classe de base : Vehicule est la classe parent.
  2. Classes dérivées : Voiture et Moto héritent de Vehicule.
  3. Fonction super() : Utilisée pour appeler des méthodes de la classe parente.
  4. Surcharge de méthode : afficher_info est 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

python
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 :

  1. Définition de __str__ : Cette méthode doit toujours retourner une chaîne de caractères.

  2. Utilisation automatique : Quand vous utilisez print(mon_livre), Python appelle automatiquement la méthode __str__ de l'objet.

  3. 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.

  4. 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__

python
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 :

  1. Livre : Représente un livre avec des attributs tels que titre, auteur, ISBN, et statut (emprunté ou disponible).
  2. Membre : Représente un membre de la bibliothèque avec des attributs comme nom, ID de membre, et liste des livres empruntés.
  3. 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