Comprendre les Fonctionnalités des Classes en Python

PythonBeginner
Pratiquer maintenant

Introduction

Dans ce laboratoire, vous acquerrez une compréhension pratique des concepts clés de la programmation orientée objet (POO) en Python. Nous commencerons par l'encapsulation, en apprenant à regrouper les données et les méthodes au sein d'une classe et à contrôler l'accès aux données à l'aide d'attributs privés.

Ensuite, vous implémenterez l'héritage pour construire des relations entre les classes, ce qui favorise la réutilisation du code. Nous explorerons également le polymorphisme, qui permet de traiter uniformément des objets de différentes classes. Enfin, vous utiliserez la méthode super() pour appeler efficacement des méthodes d'une classe parente, et vous pratiquerez l'héritage multiple pour voir comment une classe peut hériter de plusieurs classes parentes.

Ceci est un Laboratoire Guidé (Guided Lab), qui fournit des instructions étape par étape pour vous aider à apprendre et à pratiquer. Suivez attentivement les instructions pour compléter chaque étape et acquérir une expérience pratique. Les données historiques montrent qu'il s'agit d'un laboratoire de niveau débutant avec un taux d'achèvement de 100%. Il a reçu un taux d'avis positifs de 100% de la part des apprenants.

Explorer l'Encapsulation avec des Classes de Base

Dans cette étape, nous allons explorer l'encapsulation, un principe fondamental de la POO. L'encapsulation consiste à regrouper les données (attributs) et les méthodes qui opèrent sur ces données au sein d'une seule unité, une classe. Elle restreint également l'accès direct à l'état interne d'un objet, ce qui aide à prévenir la modification accidentelle des données.

En Python, nous utilisons une convention de nommage pour indiquer qu'un attribut est "privé". Le fait de préfixer un attribut par un seul trait de soulignement (underscore, ex: _name) signale qu'il est destiné à un usage interne. Bien que cela ne soit pas strictement appliqué, c'est une convention forte que les développeurs respectent.

Nous allons commencer par créer deux classes distinctes, Dog et Cat, pour voir comment elles peuvent être structurées.

Tout d'abord, localisez le fichier animal_classes.py dans l'explorateur de fichiers sur le côté gauche du WebIDE. Ouvrez-le et ajoutez le code Python suivant. Ce code définit une classe Dog et une classe Cat, chacune possédant un attribut privé _name et des méthodes pour interagir avec lui.

## File: animal_classes.py

class Dog:
    def __init__(self, name):
        ## Un préfixe avec un seul underscore indique un attribut "privé".
        self._name = name

    ## Méthode publique pour obtenir la valeur de l'attribut privé.
    def get_name(self):
        return self._name

    ## Méthode publique pour définir la valeur de l'attribut privé.
    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} says: Woof!")

class Cat:
    def __init__(self, name):
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} says: Meow!")

## Ce bloc ne s'exécutera que lorsque le script est exécuté directement.
if __name__ == "__main__":
    ## Créer une instance de la classe Dog
    my_dog = Dog("Buddy")
    print(f"Initial dog name: {my_dog.get_name()}")

    ## Changer le nom du chien en utilisant la méthode setter
    my_dog.set_name("Rocky")
    print(f"New dog name: {my_dog.get_name()}")
    my_dog.say()

    print("-" * 20)

    ## Créer une instance de la classe Cat
    my_cat = Cat("Whiskers")
    print(f"Cat name: {my_cat.get_name()}")
    my_cat.say()

Après avoir ajouté le code, enregistrez le fichier.

Maintenant, exécutons le script pour voir l'encapsulation en action. Ouvrez le terminal dans le WebIDE et exécutez la commande suivante :

python animal_classes.py

Vous verrez la sortie suivante, qui montre que nous interagissons avec l'attribut privé _name via nos méthodes publiques get_name et set_name.

Initial dog name: Buddy
New dog name: Rocky
Rocky says: Woof!
--------------------
Cat name: Whiskers
Whiskers says: Meow!

Implémenter l'Héritage et le Polymorphisme

Dans l'étape précédente, vous avez peut-être remarqué que les classes Dog et Cat partageaient beaucoup de code identique (__init__, get_name, set_name). C'est une occasion parfaite pour utiliser l'héritage. L'héritage permet à une nouvelle classe (l'enfant ou sous-classe) d'hériter des attributs et des méthodes d'une classe existante (le parent ou super-classe), favorisant ainsi la réutilisation du code.

Nous allons également introduire le polymorphisme, qui signifie "plusieurs formes". En POO, cela fait référence à la capacité de différentes classes à répondre au même appel de méthode de leur propre manière unique.

Refactorisons notre code. Nous allons créer une classe parente Animal pour contenir le code commun et faire en sorte que Dog et Cat en héritent. La méthode say, qui est différente pour chacun, démontrera le polymorphisme.

Ouvrez le fichier animal_classes.py et remplacez son contenu intégral par le code suivant :

## File: animal_classes.py

class Animal:
    def __init__(self, name):
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} makes a generic animal sound.")

## Dog hérite de Animal
class Dog(Animal):
    ## Ceci surcharge la méthode say() de la classe Animal
    def say(self):
        print(f"{self._name} says: Woof!")

## Cat hérite de Animal
class Cat(Animal):
    ## Ceci surcharge également la méthode say()
    def say(self):
        print(f"{self._name} says: Meow!")

def make_animal_speak(animal_instance):
    animal_instance.say()

if __name__ == "__main__":
    generic_animal = Animal("Creature")
    my_dog = Dog("Buddy")
    my_cat = Cat("Whiskers")

    print("--- Calling say() on each object ---")
    generic_animal.say()
    my_dog.say()
    my_cat.say()

    print("\n--- Demonstrating Polymorphism ---")
    make_animal_speak(generic_animal)
    make_animal_speak(my_dog)
    make_animal_speak(my_cat)

Enregistrez le fichier. Remarquez à quel point les classes Dog et Cat sont maintenant beaucoup plus simples. Elles héritent des méthodes __init__, get_name et set_name de Animal. Chacune fournit sa propre version de la méthode say, ce qui est un exemple de surcharge de méthode (method overriding).

Maintenant, exécutez le script mis à jour depuis le terminal :

python animal_classes.py

Le résultat sera :

--- Calling say() on each object ---
Creature makes a generic animal sound.
Buddy says: Woof!
Whiskers says: Meow!

--- Demonstrating Polymorphism ---
Creature makes a generic animal sound.
Buddy says: Woof!
Whiskers says: Meow!

La fonction make_animal_speak accepte n'importe quel objet possédant une méthode say. Même si nous lui passons différents types d'objets (Animal, Dog, Cat), elle fonctionne correctement car chaque objet sait comment exécuter l'action say à sa manière. C'est la puissance du polymorphisme.

Utiliser la Méthode super() pour Étendre les Fonctionnalités

Lorsqu'une classe enfant surcharge une méthode de sa classe parente, elle a parfois besoin d'étendre la méthode du parent, et pas seulement de la remplacer. La fonction super() fournit un moyen d'appeler la méthode de la classe parente depuis l'intérieur de la classe enfant.

Ceci est très courant dans la méthode __init__. Une classe enfant a souvent besoin d'effectuer ses propres étapes d'initialisation en plus de l'initialisation effectuée par son parent.

Ajoutons des attributs uniques à nos classes Dog et Cat. Le Dog aura un age (âge), et le Cat aura une color (couleur). Nous utiliserons super() pour nous assurer que la méthode __init__ de la classe parente Animal est toujours appelée pour définir l'attribut _name.

Modifiez le fichier animal_classes.py en remplaçant son contenu par le code suivant :

## File: animal_classes.py

class Animal:
    def __init__(self, name):
        print(f"Animal __init__ called for {name}")
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        self._name = value

    def say(self):
        print(f"{self._name} makes a generic animal sound.")

class Dog(Animal):
    def __init__(self, name, age):
        ## Appeler la méthode __init__ du parent pour gérer l'attribut 'name'
        super().__init__(name)
        print("Dog __init__ called")
        self.age = age

    def say(self):
        ## Nous pouvons également utiliser super() pour appeler la méthode say() du parent
        ## super().say()
        print(f"{self._name} says: Woof! I am {self.age} years old.")

class Cat(Animal):
    def __init__(self, name, color):
        ## Appeler la méthode __init__ du parent
        super().__init__(name)
        print("Cat __init__ called")
        self.color = color

    def say(self):
        print(f"{self._name} says: Meow! I have {self.color} fur.")

if __name__ == "__main__":
    my_dog = Dog("Buddy", 5)
    my_dog.say()

    print("-" * 20)

    my_cat = Cat("Whiskers", "black")
    my_cat.say()

Enregistrez le fichier. Dans cette version, Dog.__init__ et Cat.__init__ appellent d'abord super().__init__(name). Cela exécute le code dans Animal.__init__, qui définit l'attribut _name. Après cela, ils procèdent à leurs propres initialisations spécifiques (self.age = age et self.color = color).

Exécutez le script depuis le terminal :

python animal_classes.py

La sortie démontre la chaîne des appels __init__ et les méthodes say étendues :

Animal __init__ called for Buddy
Dog __init__ called
Buddy says: Woof! I am 5 years old.
--------------------
Animal __init__ called for Whiskers
Cat __init__ called
Whiskers says: Meow! I have black fur.

Pratiquer l'Héritage Multiple

Python permet à une classe d'hériter de plusieurs classes parentes. Ceci est appelé l'héritage multiple. Cela peut être un outil puissant pour combiner des fonctionnalités provenant de différentes sources, mais cela introduit également de la complexité, notamment dans la manière dont Python décide quelle méthode du parent utiliser si elles portent le même nom.

Cet ordre de recherche est appelé l'Ordre de Résolution des Méthodes (ORM) (Method Resolution Order - MRO). Python utilise un algorithme appelé linéarisation C3 pour déterminer un MRO cohérent et prévisible.

Explorons cela avec un nouvel exemple. Ouvrez le fichier multiple_inheritance.py dans l'explorateur de fichiers et ajoutez le code suivant :

## File: multiple_inheritance.py

class ParentA:
    def speak(self):
        print("Speaking from ParentA")

    def common_method(self):
        print("ParentA's common method")

class ParentB:
    def speak(self):
        print("Speaking from ParentB")

    def common_method(self):
        print("ParentB's common method")

## Child hérite de A, puis de B
class Child_AB(ParentA, ParentB):
    pass

## Child hérite de B, puis de A
class Child_BA(ParentB, ParentA):
    def common_method(self):
        print("Child_BA's own common method")

if __name__ == "__main__":
    child1 = Child_AB()
    child2 = Child_BA()

    print("--- Investigating Child_AB (ParentA, ParentB) ---")
    child1.speak()
    child1.common_method()
    ## La méthode .mro() montre l'Ordre de Résolution des Méthodes
    print("MRO for Child_AB:", [c.__name__ for c in Child_AB.mro()])

    print("\n--- Investigating Child_BA (ParentB, ParentA) ---")
    child2.speak()
    child2.common_method()
    print("MRO for Child_BA:", [c.__name__ for c in Child_BA.mro()])

Enregistrez le fichier. Ici, Child_AB hérite de ParentA puis de ParentB. Child_BA hérite dans l'ordre inverse. Lorsqu'une méthode est appelée, Python la recherche dans l'ordre spécifié par le MRO.

Exécutez le script depuis le terminal :

python multiple_inheritance.py

Vous verrez la sortie suivante :

--- Investigating Child_AB (ParentA, ParentB) ---
Speaking from ParentA
ParentA's common method
MRO for Child_AB: ['Child_AB', 'ParentA', 'ParentB', 'object']

--- Investigating Child_BA (ParentB, ParentA) ---
Speaking from ParentB
Child_BA's own common method
MRO for Child_BA: ['Child_BA', 'ParentB', 'ParentA', 'object']

D'après la sortie, vous pouvez observer :

  • child1.speak() appelle la méthode de ParentA car ParentA apparaît en premier dans le MRO de Child_AB.
  • child2.speak() appelle la méthode de ParentB car ParentB apparaît en premier dans le MRO de Child_BA.
  • child2.common_method() appelle la version définie directement dans Child_BA, car Python la trouve là en premier avant de vérifier les parents.

Comprendre le MRO est crucial pour prédire le comportement dans les scénarios d'héritage multiple.

Résumé

Dans ce laboratoire, vous avez acquis une expérience pratique avec quatre concepts fondamentaux de la programmation orientée objet en Python.

Vous avez commencé par l'encapsulation, apprenant à protéger les données de la classe par convention en utilisant des attributs privés et en fournissant des méthodes publiques pour y accéder. Vous avez ensuite refactorisé votre code pour utiliser l'héritage, créant une classe parente Animal afin de réduire la duplication de code dans les sous-classes Dog et Cat.

En mettant en œuvre l'héritage, vous avez vu le polymorphisme en action, car les objets Dog et Cat ont répondu différemment au même appel de méthode say(). Vous avez appris à utiliser la méthode super() pour appeler et étendre les fonctionnalités d'une classe parente, en particulier dans la méthode __init__. Enfin, vous avez exploré l'héritage multiple et l'importance de l'Ordre de Résolution des Méthodes (MRO) pour déterminer quelle méthode du parent est appelée.