Python & AI Tutorials Logo
Programmation Python

32. Étendre les classes avec l’héritage et le polymorphisme

Dans le Chapitre 30, nous avons appris à créer nos propres classes pour modéliser des concepts du monde réel. Nous avons construit des classes comme BankAccount et Student qui regroupaient données et comportement. Mais que se passe-t-il lorsque vous devez créer une nouvelle classe qui ressemble à une classe existante, mais avec quelques différences ou ajouts ?

L’héritage (inheritance) est le mécanisme de Python pour créer de nouvelles classes à partir de classes existantes. Au lieu de copier-coller du code, vous pouvez créer une sous-classe (subclass) qui récupère automatiquement tous les attributs et méthodes d’une classe parente (parent class) (aussi appelée classe de base (base class) ou superclasse (superclass)), puis ajouter ou modifier ce dont vous avez besoin.

Ce chapitre explore comment l’héritage vous permet de construire des hiérarchies de classes liées, comment personnaliser le comportement hérité, et comment le polymorphisme (polymorphism) permet d’utiliser différentes classes de manière interchangeable lorsqu’elles partagent des interfaces communes.

32.1) Créer des sous-classes à partir de classes existantes

32.1.1) La syntaxe de base de l’héritage

L’héritage vous permet de créer une nouvelle classe (appelée une sous-classe ou classe enfant) basée sur une classe existante (appelée une classe parente ou classe de base). La sous-classe hérite automatiquement de toutes les méthodes et de tous les attributs de la classe parente.

Lorsque vous créez une sous-classe, vous indiquez la classe parente entre parenthèses après le nom de la classe.

python
# Classe parente
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} makes a sound"
 
# Sous-classe héritant de Animal
class Dog(Animal):
    pass  # Aucun code supplémentaire n’est nécessaire pour l’instant
 
# Créer une instance de Dog
buddy = Dog("Buddy")
print(buddy.speak())  # Output: Buddy makes a sound
print(buddy.name)     # Output: Buddy

Même si Dog n’a aucun code propre (juste pass), elle hérite de tout depuis Animal. La classe Dog a automatiquement la méthode __init__ et la méthode speak de son parent.

Animal

+name

+init(name)

+speak()

Dog

32.1.2) Pourquoi l’héritage est important

L’héritage résout un problème courant en programmation : la duplication de code. Imaginez que vous construisez un système pour gérer différents types d’employés :

python
# Sans héritage - beaucoup de duplication
class FullTimeEmployee:
    def __init__(self, name, employee_id, salary):
        self.name = name
        self.employee_id = employee_id
        self.salary = salary
    
    def get_info(self):
        return f"{self.name} (ID: {self.employee_id})"
 
class PartTimeEmployee:
    def __init__(self, name, employee_id, hourly_rate):
        self.name = name
        self.employee_id = employee_id
        self.hourly_rate = hourly_rate
    
    def get_info(self):
        return f"{self.name} (ID: {self.employee_id})"

Remarquez que name, employee_id et get_info() sont dupliqués. Avec l’héritage, nous pouvons éliminer cette duplication :

python
# Avec héritage - code partagé dans la classe parente
class Employee:
    def __init__(self, name, employee_id):
        self.name = name
        self.employee_id = employee_id
    
    def get_info(self):
        return f"{self.name} (ID: {self.employee_id})"
 
class FullTimeEmployee(Employee):
    def __init__(self, name, employee_id, salary):
        Employee.__init__(self, name, employee_id)  # Appeler le __init__ du parent
        # Note : Nous apprendrons une meilleure façon de faire avec super() dans la section 32.3
        self.salary = salary
 
class PartTimeEmployee(Employee):
    def __init__(self, name, employee_id, hourly_rate):
        Employee.__init__(self, name, employee_id)  # Appeler le __init__ du parent
        self.hourly_rate = hourly_rate
 
# Les deux sous-classes héritent de get_info()
alice = FullTimeEmployee("Alice", "E001", 75000)
bob = PartTimeEmployee("Bob", "E002", 25)
 
print(alice.get_info())  # Output: Alice (ID: E001)
print(bob.get_info())    # Output: Bob (ID: E002)

Désormais, les attributs et méthodes communs vivent dans Employee, et chaque sous-classe ne définit que ce qui la rend unique.

32.1.3) Ajouter de nouvelles méthodes aux sous-classes

Les sous-classes peuvent ajouter des méthodes qui n’existent pas dans la classe parente :

python
class Vehicle:
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model
    
    def get_description(self):
        return f"{self.brand} {self.model}"
 
class Car(Vehicle):
    def __init__(self, brand, model, num_doors):
        Vehicle.__init__(self, brand, model)
        self.num_doors = num_doors
    
    def honk(self):  # Nouvelle méthode spécifique à Car
        return "Beep beep!"
 
class Motorcycle(Vehicle):
    def __init__(self, brand, model, has_sidecar):
        Vehicle.__init__(self, brand, model)
        self.has_sidecar = has_sidecar
    
    def rev_engine(self):  # Nouvelle méthode spécifique à Motorcycle
        return "Vroom vroom!"
 
# Chaque sous-classe a les méthodes de son parent plus les siennes
my_car = Car("Toyota", "Camry", 4)
print(my_car.get_description())  # Output: Toyota Camry
print(my_car.honk())             # Output: Beep beep!
 
my_bike = Motorcycle("Harley", "Sportster", False)
print(my_bike.get_description())  # Output: Harley Sportster
print(my_bike.rev_engine())       # Output: Vroom vroom!

La classe Car possède à la fois get_description() (héritée) et honk() (propre). La classe Motorcycle possède get_description() (héritée) et rev_engine() (propre).

32.1.4) Ajouter de nouveaux attributs aux sous-classes

Les sous-classes peuvent aussi ajouter leurs propres attributs d’instance. Vous faites généralement cela dans la méthode __init__ de la sous-classe :

python
class BankAccount:
    def __init__(self, account_number, balance):
        self.account_number = account_number
        self.balance = balance
    
    def deposit(self, amount):
        self.balance += amount
        return self.balance
 
class SavingsAccount(BankAccount):
    def __init__(self, account_number, balance, interest_rate):
        BankAccount.__init__(self, account_number, balance)
        self.interest_rate = interest_rate  # Nouvel attribut
    
    def apply_interest(self):  # Nouvelle méthode utilisant le nouvel attribut
        interest = self.balance * self.interest_rate
        self.balance += interest
        return interest
 
# SavingsAccount a tous les attributs de BankAccount plus interest_rate
savings = SavingsAccount("SA001", 1000, 0.03)
savings.deposit(500)  # Méthode héritée
print(f"Balance: ${savings.balance}")  # Output: Balance: $1500
 
interest_earned = savings.apply_interest()
print(f"Interest earned: ${interest_earned:.2f}")  # Output: Interest earned: $45.00
print(f"New balance: ${savings.balance:.2f}")      # Output: New balance: $1545.00

Le SavingsAccount a account_number et balance depuis BankAccount, plus son propre attribut interest_rate.

32.1.5) Plusieurs niveaux d’héritage

Les classes peuvent hériter de classes qui elles-mêmes héritent d’autres classes, créant une hiérarchie d’héritage :

python
class LivingThing:
    def __init__(self, name):
        self.name = name
    
    def is_alive(self):
        return True
 
class Animal(LivingThing):
    def __init__(self, name, species):
        LivingThing.__init__(self, name)
        self.species = species
    
    def move(self):
        return f"{self.name} is moving"
 
class Dog(Animal):
    def __init__(self, name, breed):
        Animal.__init__(self, name, "Dog")
        self.breed = breed
    
    def bark(self):
        return f"{self.name} says: Woof!"
 
# Dog hérite de Animal, qui hérite de LivingThing
max_dog = Dog("Max", "Golden Retriever")
 
# Les méthodes des trois niveaux fonctionnent
print(max_dog.is_alive())  # Output: True (from LivingThing)
print(max_dog.move())      # Output: Max is moving (from Animal)
print(max_dog.bark())      # Output: Max says: Woof! (from Dog)
 
# Les attributs des trois niveaux existent
print(max_dog.name)     # Output: Max (from LivingThing)
print(max_dog.species)  # Output: Dog (from Animal)
print(max_dog.breed)    # Output: Golden Retriever (from Dog)

LivingThing

+name

+is_alive()

Animal

+species

+move()

Dog

+breed

+bark()

Dog hérite de Animal, qui hérite de LivingThing. Cela signifie que Dog a accès aux méthodes et attributs des deux classes parentes.

32.2) Redéfinir des méthodes dans les sous-classes

32.2.1) Ce que signifie la redéfinition de méthode

Parfois, une sous-classe doit modifier le fonctionnement d’une méthode héritée. La redéfinition de méthode (method overriding) signifie définir une méthode dans la sous-classe avec le même nom qu’une méthode dans la classe parente. Lorsque vous appelez cette méthode sur une instance de la sous-classe, Python utilise la version de la sous-classe à la place de celle du parent.

python
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} makes a sound"
 
class Dog(Animal):
    def speak(self):  # Redéfinir la méthode speak du parent
        return f"{self.name} says: Woof!"
 
class Cat(Animal):
    def speak(self):  # Redéfinir avec un comportement différent
        return f"{self.name} says: Meow!"
 
# Chaque classe a sa propre version de speak()
generic_animal = Animal("Generic")
print(generic_animal.speak())  # Output: Generic makes a sound
 
buddy = Dog("Buddy")
print(buddy.speak())  # Output: Buddy says: Woof!
 
whiskers = Cat("Whiskers")
print(whiskers.speak())  # Output: Whiskers says: Meow!

Lorsque vous appelez buddy.speak(), Python cherche d’abord la méthode speak dans la classe Dog, puisque buddy est une instance de Dog. Comme Dog définit sa propre méthode speak, Python utilise cette version. Si Dog n’avait pas de méthode speak, Python chercherait alors dans la classe parente Animal et utiliserait cette version à la place.

Cet ordre de recherche — en commençant par la classe de l’instance, puis en remontant vers la classe parente — est la façon dont la redéfinition de méthode fonctionne et dont les sous-classes personnalisent le comportement hérité.

32.2.2) Pourquoi redéfinir des méthodes ?

La redéfinition de méthode permet de créer des versions spécialisées d’un comportement général. Considérez une hiérarchie de formes :

python
class Shape:
    def __init__(self, name):
        self.name = name
    
    def area(self):
        return 0  # Implémentation par défaut
    
    def describe(self):
        return f"{self.name} with area {self.area()}"
 
class Rectangle(Shape):
    def __init__(self, width, height):
        Shape.__init__(self, "Rectangle")
        self.width = width
        self.height = height
    
    def area(self):  # Redéfinir avec un calcul spécifique au rectangle
        return self.width * self.height
 
class Circle(Shape):
    def __init__(self, radius):
        Shape.__init__(self, "Circle")
        self.radius = radius
    
    def area(self):  # Redéfinir avec un calcul spécifique au cercle
        return 3.14159 * self.radius ** 2
 
# Chaque forme calcule l’aire différemment
rect = Rectangle(5, 3)
print(rect.describe())  # Output: Rectangle with area 15
 
circle = Circle(4)
print(circle.describe())  # Output: Circle with area 50.26544

La méthode describe() est héritée par les deux sous-classes et fonctionne correctement parce que chaque sous-classe fournit sa propre implémentation de area().

Lorsque vous appelez rect.describe(), la méthode héritée describe() s’exécute, mais self fait référence à l’instance Rectangle. Ainsi, lorsque describe() appelle self.area(), Python cherche d’abord area() dans la classe Rectangle et trouve la version redéfinie.

32.2.3) Redéfinir __init__ et appeler l’initialisation parente

Lorsque vous redéfinissez __init__, vous devez généralement appeler le __init__ du parent pour vous assurer que l’initialisation du parent a lieu :

python
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def introduce(self):
        return f"I'm {self.name}, {self.age} years old"
 
class Student(Person):
    def __init__(self, name, age, student_id, major):
        # Appeler le __init__ du parent pour définir name et age
        Person.__init__(self, name, age)
        # Puis définir les attributs spécifiques à Student
        self.student_id = student_id
        self.major = major
    
alice = Student("Alice", 20, "S12345", "Computer Science")
print(alice.name)        # Output: Alice
print(alice.student_id)  # Output: S12345

Remarquez comment Student.__init__ appelle d’abord Person.__init__(self, name, age) pour initialiser les attributs de la classe parente, puis ajoute ses propres attributs.

32.3) Utiliser super() pour accéder au comportement du parent

32.3.1) Ce que fait super()

Dans la section précédente, nous avons appelé des méthodes parentes explicitement : ParentClass.method(self, ...). Python fournit une manière plus propre : la fonction super(). super() renvoie un objet temporaire qui vous permet d’appeler des méthodes de la classe parente sans nommer explicitement le parent.

python
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} makes a sound"
 
class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)  # Plus propre que Animal.__init__(self, name)
        self.breed = breed
    
    def speak(self):
        parent_sound = super().speak()  # Appeler speak() du parent
        return f"{parent_sound} - specifically, Woof!"
 
buddy = Dog("Buddy", "Labrador")
print(buddy.speak())
# Output: Buddy makes a sound - specifically, Woof!

Utiliser super() présente plusieurs avantages :

  • Vous n’avez pas besoin de nommer la classe parente explicitement
  • Cela fonctionne correctement avec l’héritage multiple (abordé plus tard)
  • Cela rend le code plus facile à maintenir si vous changez la classe parente

32.3.2) Utiliser super() dans __init__

L’utilisation la plus courante de super() est l’appel du __init__ du parent :

python
class Employee:
    def __init__(self, name, employee_id):
        self.name = name
        self.employee_id = employee_id
        self.is_active = True
    
    def deactivate(self):
        self.is_active = False
 
class Manager(Employee):
    def __init__(self, name, employee_id, department):
        super().__init__(name, employee_id)  # Initialiser les attributs du parent
        self.department = department
        self.team = []  # Attribut spécifique à Manager
    
    def add_team_member(self, employee):
        self.team.append(employee)
 
# Manager récupère tous les attributs de Employee plus les siens
sarah = Manager("Sarah", "M001", "Engineering")
print(sarah.name)        # Output: Sarah
print(sarah.is_active)   # Output: True
print(sarah.department)  # Output: Engineering

En appelant super().__init__(name, employee_id), la classe Manager s’assure que toute la logique d’initialisation de Employee s’exécute, y compris la mise de is_active à True.

32.3.3) Étendre des méthodes parentes avec super()

Vous pouvez utiliser super() pour étendre une méthode parente plutôt que la remplacer complètement :

python
class BankAccount:
    def __init__(self, account_number, balance):
        self.account_number = account_number
        self.balance = balance
        self.transaction_count = 0
    
    def deposit(self, amount):
        self.balance += amount
        self.transaction_count += 1
        return self.balance
 
class CheckingAccount(BankAccount):
    def __init__(self, account_number, balance, overdraft_limit):
        super().__init__(account_number, balance)
        self.overdraft_limit = overdraft_limit
    
    def deposit(self, amount):
        was_overdrawn = self.balance < 0
        
        # Appeler deposit du parent pour gérer la logique de base
        new_balance = super().deposit(amount)
        
        # Ajouter un comportement spécifique à CheckingAccount
        if was_overdrawn and new_balance >= 0:
            print("Account is no longer overdrawn")
        return new_balance
 
checking = CheckingAccount("C001", -50, 100)
checking.deposit(75)
# Output: Account is no longer overdrawn
print(f"Balance: ${checking.balance}")  # Output: Balance: $25
print(f"Transactions: {checking.transaction_count}")  # Output: Transactions: 1

La méthode CheckingAccount.deposit() appelle super().deposit(amount) pour gérer la logique de base du dépôt (mettre à jour le solde et le nombre de transactions), puis ajoute sa propre vérification du statut de découvert.

32.3.4) Quand utiliser super() vs un appel direct au parent

Utilisez super() dans la plupart des cas :

python
class Vehicle:
    def __init__(self, brand):
        self.brand = brand
 
class Car(Vehicle):
    def __init__(self, brand, model):
        super().__init__(brand)  # Préféré
        self.model = model

Utilisez des appels directs au parent lorsque vous devez appeler un parent spécifique dans un scénario d’héritage multiple (abordé plus tard) ou lorsque vous voulez être explicite sur le parent que vous appelez :

python
class Car(Vehicle):
    def __init__(self, brand, model):
        Vehicle.__init__(self, brand)  # Explicite, mais moins flexible
        self.model = model

Pour un héritage simple (un parent), super() est presque toujours le meilleur choix.

32.3.5) super() avec d’autres méthodes

Vous pouvez utiliser super() avec n’importe quelle méthode, pas seulement __init__ :

python
class TextProcessor:
    def process(self, text):
        # Traitement de base : supprimer les espaces
        return text.strip()
 
class UppercaseProcessor(TextProcessor):
    def process(self, text):
        # D’abord, effectuer le traitement du parent
        processed = super().process(text)
        # Puis ajouter la conversion en majuscules
        return processed.upper()
 
class PrefixProcessor(UppercaseProcessor):
    def __init__(self, prefix):
        self.prefix = prefix
    
    def process(self, text):
        # D’abord, effectuer le traitement du parent (qui appelle aussi son parent)
        processed = super().process(text)
        # Puis ajouter le préfixe
        return f"{self.prefix}: {processed}"
 
processor = PrefixProcessor("ALERT")
result = processor.process("  system error  ")
print(result)  # Output: ALERT: SYSTEM ERROR

32.4) Polymorphisme : travailler avec des classes compatibles

32.4.1) Ce que signifie le polymorphisme

Le polymorphisme (du grec : « nombreuses formes ») est la capacité de traiter de la même manière des objets de classes différentes s’ils fournissent les mêmes méthodes.

En Python, si plusieurs classes ont des méthodes avec le même nom, vous pouvez appeler ces méthodes sans connaître la classe exacte de l’objet.

python
class Dog:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} says: Woof!"
 
class Cat:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} says: Meow!"
 
class Bird:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} says: Tweet!"
 
# Fonction qui fonctionne avec tout objet ayant une méthode speak()
def make_animal_speak(animal):
    print(animal.speak())
 
# Fonctionne avec différentes classes
buddy = Dog("Buddy")
whiskers = Cat("Whiskers")
tweety = Bird("Tweety")
 
make_animal_speak(buddy)     # Output: Buddy says: Woof!
make_animal_speak(whiskers)  # Output: Whiskers says: Meow!
make_animal_speak(tweety)    # Output: Tweety says: Tweet!

La fonction make_animal_speak() ne se soucie pas de la classe du paramètre animal — elle a seulement besoin que l’objet ait une méthode speak(). C’est le polymorphisme en action.

32.4.2) Polymorphisme avec l’héritage

Le polymorphisme est particulièrement puissant avec l’héritage, où les sous-classes redéfinissent les méthodes parentes :

python
class PaymentMethod:
    def process_payment(self, amount):
        return f"Processing ${amount:.2f}"
 
class CreditCard(PaymentMethod):
    def __init__(self, card_number):
        self.card_number = card_number
    
    def process_payment(self, amount):
        return f"Charging ${amount:.2f} to credit card ending in {self.card_number[-4:]}"
 
class PayPal(PaymentMethod):
    def __init__(self, email):
        self.email = email
    
    def process_payment(self, amount):
        return f"Sending ${amount:.2f} via PayPal to {self.email}"
 
class BankTransfer(PaymentMethod):
    def __init__(self, account_number):
        self.account_number = account_number
    
    def process_payment(self, amount):
        return f"Transferring ${amount:.2f} to account {self.account_number}"
 
# Fonction qui fonctionne avec n’importe quel PaymentMethod
def complete_purchase(payment_method, amount):
    print(payment_method.process_payment(amount))
    print("Purchase complete!")
 
# Tous les moyens de paiement fonctionnent avec la même fonction
credit = CreditCard("1234567890123456")
paypal = PayPal("user@example.com")
bank = BankTransfer("9876543210")
 
complete_purchase(credit, 99.99)
# Output: Charging $99.99 to credit card ending in 3456
# Output: Purchase complete!
 
complete_purchase(paypal, 49.50)
# Output: Sending $49.50 via PayPal to user@example.com
# Output: Purchase complete!
 
complete_purchase(bank, 199.00)
# Output: Transferring $199.00 to account 9876543210
# Output: Purchase complete!

La fonction complete_purchase() fonctionne avec n’importe quelle sous-classe de PaymentMethod. Chaque sous-classe fournit sa propre implémentation de process_payment(), mais la fonction n’a pas besoin de savoir avec quelle classe spécifique elle travaille.

32.4.3) Duck typing : « Si ça marche comme un canard... »

Le polymorphisme de Python n’exige pas que les classes soient liées par héritage. Cela s’appelle le duck typing (duck typing) : « Si ça marche comme un canard et que ça cancane comme un canard, alors c’est un canard. » Autrement dit, Python se soucie de ce qu’un objet peut faire (ses méthodes), pas de ce qu’il est (sa classe). Si un objet possède les méthodes dont vous avez besoin, vous pouvez l’utiliser, quelle que soit sa hiérarchie de classes.

python
class FileWriter:
    def __init__(self, filename):
        self.filename = filename
    
    def write(self, data):
        print(f"Writing to {self.filename}: {data}")
 
class DatabaseWriter:
    def __init__(self, table_name):
        self.table_name = table_name
    
    def write(self, data):
        print(f"Inserting into {self.table_name}: {data}")
 
class ConsoleWriter:
    def write(self, data):
        print(f"Console output: {data}")
 
# Fonction qui fonctionne avec tout objet ayant une méthode write()
def save_data(writer, data):
    writer.write(data)
 
# Les trois classes fonctionnent, même si elles ne sont pas liées
file_writer = FileWriter("data.txt")
db_writer = DatabaseWriter("users")
console_writer = ConsoleWriter()
 
save_data(file_writer, "User data")
# Output: Writing to data.txt: User data
 
save_data(db_writer, "User data")
# Output: Inserting into users: User data
 
save_data(console_writer, "User data")
# Output: Console output: User data

Aucune de ces classes n’hérite d’un parent commun, mais elles fonctionnent toutes avec save_data() parce qu’elles ont toutes une méthode write(). C’est le duck typing — la fonction ne se soucie pas de la classe, seulement de l’interface (les méthodes disponibles).

32.4.4) Exemple pratique : un système de plugins

Le polymorphisme permet des systèmes flexibles et extensibles. Voici un système de plugins simple pour le traitement de données :

python
class DataProcessor:
    def process(self, data):
        return data  # L’implémentation de base ne fait rien
 
class UppercaseProcessor(DataProcessor):
    def process(self, data):
        return data.upper()
 
class ReverseProcessor(DataProcessor):
    def process(self, data):
        return data[::-1]
 
class RemoveSpacesProcessor(DataProcessor):
    def process(self, data):
        return data.replace(" ", "")
 
class DataPipeline:
    def __init__(self):
        self.processors = []
    
    def add_processor(self, processor):
        self.processors.append(processor)
    
    def run(self, data):
        result = data
        for processor in self.processors:
            result = processor.process(result)  # Appel polymorphe
        return result
 
# Construire un pipeline de traitement
pipeline = DataPipeline()
pipeline.add_processor(UppercaseProcessor())
pipeline.add_processor(RemoveSpacesProcessor())
pipeline.add_processor(ReverseProcessor())
 
# Traiter les données via le pipeline
input_data = "Hello World"
output = pipeline.run(input_data)
print(f"Input:  {input_data}")   # Output: Input:  Hello World
print(f"Output: {output}")        # Output: Output: DLROWOLLEH

Le DataPipeline n’a pas besoin de savoir quels processeurs spécifiques il contient — il appelle simplement process() sur chacun. Vous pouvez facilement ajouter de nouveaux types de processeurs sans modifier le code du pipeline.

32.5) Vérifier les types et les relations entre classes (isinstance, issubclass)

32.5.1) Vérifier les types d’instance avec isinstance()

Parfois, vous devez vérifier si un objet est une instance d’une classe particulière. La fonction isinstance() fait cela :

python
class Animal:
    pass
 
class Dog(Animal):
    pass
 
class Cat(Animal):
    pass
 
buddy = Dog()
whiskers = Cat()
 
# Vérifier si un objet est une instance d’une classe
print(isinstance(buddy, Dog))     # Output: True
print(isinstance(buddy, Animal))  # Output: True (Dog inherits from Animal)
print(isinstance(buddy, Cat))     # Output: False
 
print(isinstance(whiskers, Cat))    # Output: True
print(isinstance(whiskers, Animal)) # Output: True
print(isinstance(whiskers, Dog))    # Output: False

Remarquez que isinstance(buddy, Animal) renvoie True même si buddy est une instance de Dog. C’est parce que Dog hérite de Animal, donc une instance de Dog est aussi considérée comme une instance de Animal.

32.5.2) Pourquoi isinstance() respecte l’héritage

La fonction isinstance() vérifie toute la chaîne d’héritage :

python
class Vehicle:
    pass
 
class Car(Vehicle):
    pass
 
class ElectricCar(Car):
    pass
 
tesla = ElectricCar()
 
# Vérifier tous les niveaux d’héritage
print(isinstance(tesla, ElectricCar))  # Output: True
print(isinstance(tesla, Car))          # Output: True
print(isinstance(tesla, Vehicle))      # Output: True
print(isinstance(tesla, str))          # Output: False

est un

est un

est un

instance tesla

ElectricCar

Car

Vehicle

L’objet tesla est une instance de ElectricCar, mais c’est aussi une instance de Car et Vehicle à cause de l’héritage.

32.5.3) Vérifier plusieurs types à la fois

Vous pouvez vérifier si un objet est une instance de l’une de plusieurs classes en passant un tuple :

python
class Dog:
    pass
 
class Cat:
    pass
 
class Bird:
    pass
 
def is_pet(animal):
    return isinstance(animal, (Dog, Cat, Bird))
 
buddy = Dog()
whiskers = Cat()
tweety = Bird()
rock = "just a rock"
 
print(is_pet(buddy))     # Output: True
print(is_pet(whiskers))  # Output: True
print(is_pet(tweety))    # Output: True
print(is_pet(rock))      # Output: False

C’est plus concis que d’écrire isinstance(animal, Dog) or isinstance(animal, Cat) or isinstance(animal, Bird).

32.5.4) Vérifier les relations entre classes avec issubclass()

La fonction issubclass() vérifie si une classe est une sous-classe d’une autre :

python
class Animal:
    pass
 
class Dog(Animal):
    pass
 
class Cat(Animal):
    pass
 
class Poodle(Dog):
    pass
 
# Vérifier les relations entre classes
print(issubclass(Dog, Animal))    # Output: True
print(issubclass(Cat, Animal))    # Output: True
print(issubclass(Poodle, Dog))    # Output: True
print(issubclass(Poodle, Animal)) # Output: True (indirect inheritance)
print(issubclass(Dog, Cat))       # Output: False
 
# Une classe est considérée comme une sous-classe d’elle-même
print(issubclass(Dog, Dog))       # Output: True

Notez que issubclass() fonctionne avec des classes, pas avec des instances. Utilisez isinstance() pour les instances et issubclass() pour les classes.

32.5.5) Cas d’usage pratiques pour la vérification de type

La vérification de type est utile lorsque vous avez besoin d’un comportement différent selon les types :

python
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
 
class Manager(Employee):
    def __init__(self, name, salary, bonus):
        super().__init__(name, salary)
        self.bonus = bonus
 
class Contractor:
    def __init__(self, name, hourly_rate, hours):
        self.name = name
        self.hourly_rate = hourly_rate
        self.hours = hours
 
def calculate_payment(worker):
    # Lorsque vous utilisez isinstance() avec l’héritage, vérifiez les sous-classes avant les classes parentes
    if isinstance(worker, Manager):  # Vérifier Manager d’abord
        return worker.salary + worker.bonus
    elif isinstance(worker, Employee):  # Puis vérifier Employee (classe parente)
        return worker.salary
    elif isinstance(worker, Contractor):
        return worker.hourly_rate * worker.hours
    else:
        return 0
 
alice = Employee("Alice", 50000)
bob = Manager("Bob", 70000, 10000)
charlie = Contractor("Charlie", 50, 160)
 
print(f"Alice's payment: ${calculate_payment(alice)}")    # Output: Alice's payment: $50000
print(f"Bob's payment: ${calculate_payment(bob)}")        # Output: Bob's payment: $80000
print(f"Charlie's payment: ${calculate_payment(charlie)}")# Output: Charlie's payment: $8000

Cependant, dans de nombreux cas, le polymorphisme (le fait que chaque classe implémente une méthode commune) est préférable à la vérification de type :

python
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
    
    def calculate_payment(self):
        return self.salary
 
class Manager(Employee):
    def __init__(self, name, salary, bonus):
        super().__init__(name, salary)
        self.bonus = bonus
    
    def calculate_payment(self):
        return self.salary + self.bonus
 
class Contractor:
    def __init__(self, name, hourly_rate, hours):
        self.name = name
        self.hourly_rate = hourly_rate
        self.hours = hours
    
    def calculate_payment(self):
        return self.hourly_rate * self.hours
 
# Aucune vérification de type nécessaire - le polymorphisme s’en charge
workers = [
    Employee("Alice", 50000),
    Manager("Bob", 70000, 10000),
    Contractor("Charlie", 50, 160)
]
 
for worker in workers:
    payment = worker.calculate_payment()  # Appel polymorphe
    print(f"{worker.name}'s payment: ${payment}")

Output:

Alice's payment: $50000
Bob's payment: $80000
Charlie's payment: $8000

Cette approche polymorphe est plus flexible et plus simple à étendre avec de nouveaux types de travailleurs. Vous n’avez pas besoin de modifier le code appelant lorsque vous ajoutez une nouvelle classe de travailleur — assurez-vous simplement qu’elle possède une méthode calculate_payment().


L’héritage et le polymorphisme sont des outils puissants pour organiser le code et créer des systèmes flexibles et extensibles. En créant des sous-classes, vous pouvez réutiliser du code existant tout en ajoutant ou modifiant des comportements. En redéfinissant des méthodes, vous pouvez personnaliser la façon dont les sous-classes fonctionnent. Et en utilisant le polymorphisme, vous pouvez écrire du code qui fonctionne avec de nombreuses classes différentes via une interface commune.

L’essentiel est d’utiliser ces fonctionnalités avec discernement :

  • Utilisez l’héritage lorsqu’il existe une véritable relation « est-un » (un Dog est un Animal)
  • Redéfinissez des méthodes pour spécialiser un comportement, pas pour changer complètement ce que fait une classe
  • Utilisez super() pour étendre le comportement du parent plutôt que le remplacer entièrement
  • Préférez le polymorphisme (méthodes communes) à la vérification de type lorsque c’est possible

Au fur et à mesure que vous construisez des programmes plus grands, ces techniques orientées objet vous aideront à créer du code plus facile à comprendre, à maintenir et à faire évoluer.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai