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.
# 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: BuddyMê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.
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 :
# 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 :
# 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 :
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 :
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.00Le 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 :
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)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.
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 :
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.26544La 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 :
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: S12345Remarquez 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.
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 :
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: EngineeringEn 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 :
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: 1La 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 :
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 = modelUtilisez 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 :
class Car(Vehicle):
def __init__(self, brand, model):
Vehicle.__init__(self, brand) # Explicite, mais moins flexible
self.model = modelPour 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__ :
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 ERROR32.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.
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 :
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.
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 dataAucune 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 :
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: DLROWOLLEHLe 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 :
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: FalseRemarquez 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 :
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: FalseL’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 :
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: FalseC’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 :
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: TrueNotez 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 :
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: $8000Cependant, 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 :
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: $8000Cette 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
Dogest unAnimal) - 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.