Héritage en Python
Apprenez l'héritage Python : sous-classes, surcharge de méthodes, super(), héritage multiple, MRO et isinstance — avec des exemples exécutables.
Comprendre l'héritage en Python
L'héritage vous permet de construire une nouvelle classe à partir d'une classe existante. La nouvelle classe (la sous-classe ou classe enfant) reçoit automatiquement tous les attributs et méthodes de la classe existante (la superclasse, classe de base ou classe parente). Vous pouvez ensuite ajouter de nouveaux comportements ou remplacer sélectivement ce que vous avez hérité.
Ce chapitre couvre :
- La syntaxe pour créer des sous-classes
- Comment
super()fonctionne et pourquoi vous devriez l'utiliser - La surcharge de méthodes et l'appel de la version parente
- L'héritage simple, multiniveau et multiple
- L'ordre de résolution des méthodes (MRO) en Python
isinstance()etissubclass()pour les vérifications de type à l'exécution- Les pièges courants
Avant de lire ce chapitre, assurez-vous d'être à l'aise avec les classes et objets Python. Pour les autres piliers de la POO, consultez l'encapsulation Python et les classes abstraites Python.
La syntaxe de l'héritage
Pour créer une sous-classe, écrivez le nom de la classe parente entre parenthèses après le nom de la nouvelle classe :
class ParentClass:
pass
class ChildClass(ParentClass):
passUn exemple minimal mais concret avec une classe de base Vehicle et une sous-classe Car :
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def start(self):
print(f"{self.make} {self.model} started.")
def stop(self):
print(f"{self.make} {self.model} stopped.")
class Car(Vehicle):
def __init__(self, make, model, year, num_doors=4):
super().__init__(make, model, year) # delegate to Vehicle.__init__
self.num_doors = num_doors
camry = Car("Toyota", "Camry", 2023)
camry.start() # Toyota Camry started.
camry.stop() # Toyota Camry stopped.
print(camry.num_doors) # 4Car hérite de start() et stop() de Vehicle sans les dupliquer. L'ajout de num_doors est la seule nouvelle préoccupation que Car doit gérer.
Utiliser super()
super() retourne un objet proxy qui délègue les appels de méthodes à la classe parente. Son utilisation la plus courante est à l'intérieur de __init__ pour initialiser les attributs du parent avant d'ajouter ceux de l'enfant :
class Car(Vehicle):
def __init__(self, make, model, year, num_doors=4):
super().__init__(make, model, year) # runs Vehicle.__init__
self.num_doors = num_doorsPourquoi préférer super() à l'écriture directe de Vehicle.__init__(self, ...) ?
- Maintenance : si vous renommez ou remplacez la classe parente, vous ne changez qu'une seule ligne.
- Héritage multiple :
super()suit l'ordre de résolution des méthodes (MRO) de Python, de sorte que chaque classe de la chaîne est appelée correctement. Coder en dur le nom du parent contourne cette logique.
Vous pouvez appeler super() pour n'importe quelle méthode, pas seulement __init__ :
class Car(Vehicle):
def start(self):
super().start() # call Vehicle.start first
print(f"({self.num_doors}-door model ready)")Surcharge de méthodes
Une sous-classe surcharge une méthode parente en définissant une méthode du même nom. Python appelle toujours la version la plus dérivée en premier :
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def start(self):
print(f"{self.make} {self.model} started.")
def stop(self):
print(f"{self.make} {self.model} stopped.")
class Car(Vehicle):
def start(self):
print(f"{self.make} {self.model} revved the engine and started.")
camry = Car("Toyota", "Camry", 2023)
camry.start() # Toyota Camry revved the engine and started.
camry.stop() # Toyota Camry stopped. (inherited, not overridden)Lorsque vous souhaitez étendre plutôt que remplacer le comportement parent, appelez super() à l'intérieur de la surcharge :
class Car(Vehicle):
def start(self):
super().start() # Vehicle.start runs first
print("Seatbelt reminder: buckle up!") # then add the extra stepHéritage des attributs de classe
L'héritage s'applique également aux attributs de classe (partagés entre les instances). Une sous-classe peut les remplacer de la même manière qu'elle surcharge les méthodes :
class Vehicle:
wheels = 4
class Motorcycle(Vehicle):
wheels = 2 # override the class attribute
class Car(Vehicle):
pass # inherits wheels = 4
print(Motorcycle.wheels) # 2
print(Car.wheels) # 4
print(Vehicle.wheels) # 4Types d'héritage
Héritage simple
Un enfant, un parent — le schéma le plus courant.
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):
return f"{self.name} says woof!"
dog = Dog("Rex")
print(dog.speak()) # Rex says woof!Héritage multiniveau
Une sous-classe peut elle-même être sous-classée, créant une chaîne :
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def stop(self):
print(f"{self.make} {self.model} stopped.")
class Car(Vehicle):
def __init__(self, make, model, year, num_doors=4):
super().__init__(make, model, year)
self.num_doors = num_doors
def start(self):
print(f"{self.make} {self.model} started.")
class ElectricCar(Car):
def __init__(self, make, model, year, range_km):
super().__init__(make, model, year) # calls Car.__init__
self.range_km = range_km
def start(self):
print(f"{self.make} {self.model} powered up silently.")
def charge(self):
print(f"Charging... range is {self.range_km} km.")
tesla = ElectricCar("Tesla", "Model 3", 2024, 580)
tesla.start() # Tesla Model 3 powered up silently.
tesla.charge() # Charging... range is 580 km.
tesla.stop() # Tesla Model 3 stopped. (inherited from Vehicle)ElectricCar se trouve deux niveaux en dessous de Vehicle. Chaque appel à super().__init__ remonte d'un niveau dans la chaîne, de sorte que les trois méthodes __init__ sont exécutées.
Héritage multiple
Python permet à une classe d'hériter de plus d'un parent. Listez-les entre parenthèses, séparés par des virgules :
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound."
class Swimmer:
def swim(self):
return f"{self.name} swims."
class Flyer:
def fly(self):
return f"{self.name} flies."
class Duck(Animal, Swimmer, Flyer):
def speak(self):
return f"{self.name} says quack!"
donald = Duck("Donald")
print(donald.speak()) # Donald says quack!
print(donald.swim()) # Donald swims.
print(donald.fly()) # Donald flies.L'héritage multiple est puissant mais introduit de la complexité. Utilisez des mixins — de petites classes à usage unique qui ajoutent une seule capacité — pour le rendre gérable. Swimmer et Flyer ci-dessus illustrent exactement ce schéma.
L'ordre de résolution des méthodes (MRO)
Lorsque Python recherche une méthode ou un attribut, il suit un ordre de recherche déterministe appelé l'ordre de résolution des méthodes (MRO). Pour l'héritage simple, l'ordre est évident (enfant → parent → grand-parent → object). Pour l'héritage multiple, Python utilise l'algorithme de linéarisation C3 pour produire un ordre non ambigu.
Inspectez le MRO de n'importe quelle classe avec .__mro__ ou help() :
print(Duck.__mro__)
# (<class 'Duck'>, <class 'Animal'>, <class 'Swimmer'>, <class 'Flyer'>, <class 'object'>)Le MRO est le plus important lorsque plusieurs parents définissent la même méthode. Python choisit la première classe dans le MRO qui la définit. C'est pourquoi super() est important dans les hiérarchies à héritage multiple : chaque classe du MRO appelle super(), de sorte que chaque classe de la chaîne a la possibilité de s'exécuter.
class Base:
def greet(self):
print("Hello from Base")
class Left(Base):
def greet(self):
print("Hello from Left")
super().greet()
class Right(Base):
def greet(self):
print("Hello from Right")
super().greet()
class Child(Left, Right):
def greet(self):
print("Hello from Child")
super().greet()
Child().greet()
# Hello from Child
# Hello from Left
# Hello from Right
# Hello from BaseChaque appel à super() suit le MRO (Child → Left → Right → Base), de sorte que chaque greet() s'exécute exactement une fois, même si Base apparaît comme parent de Left et de Right. C'est le problème du diamant — le MRO de Python le résout proprement.
Vérifier l'héritage à l'exécution
isinstance(obj, cls)
Retourne True si obj est une instance de cls ou de l'une de ses sous-classes :
tesla = ElectricCar("Tesla", "Model 3", 2024, 580)
print(isinstance(tesla, ElectricCar)) # True
print(isinstance(tesla, Car)) # True — Car is a parent
print(isinstance(tesla, Vehicle)) # True — Vehicle is a grandparent
print(isinstance(tesla, str)) # FalseC'est plus fiable que de comparer type(obj) == Car, ce qui retourne False pour les sous-classes.
issubclass(sub, cls)
Retourne True si sub est une sous-classe de cls (y compris elle-même) :
print(issubclass(ElectricCar, Vehicle)) # True
print(issubclass(Car, ElectricCar)) # False
print(issubclass(Car, Car)) # True — a class is a subclass of itselfPièges courants
Oublier d'appeler super().__init__()
Si le __init__ de l'enfant n'appelle pas super().__init__(), les attributs du parent ne sont jamais définis. Toute méthode qui les attend lèvera une AttributeError :
class Car(Vehicle):
def __init__(self, make, model, year, num_doors=4):
# forgot super().__init__(...)
self.num_doors = num_doors
c = Car("Toyota", "Camry", 2023)
c.start() # AttributeError: 'Car' object has no attribute 'make'Surcharger sans appeler super() quand on voulait étendre
Si vous surchargez une méthode et oubliez super(), la version parente ne s'exécute jamais. Cela supprime silencieusement un comportement dont d'autres parties du code peuvent dépendre.
Hiérarchies d'héritage multiple trop profondes
Plus de deux niveaux d'héritage multiple est très difficile à raisonner. Si vous vous retrouvez à écrire class Foo(A, B, C, D), envisagez d'utiliser la composition — stocker des instances comme attributs — à la place.
Avantages de l'héritage
- Réutilisation du code — la logique partagée se trouve en un seul endroit ; les sous-classes en héritent gratuitement.
- Extensibilité — vous pouvez ajouter ou modifier un comportement dans une sous-classe sans toucher au parent, ce qui laisse les appelants existants inchangés.
- Polymorphisme — les fonctions qui acceptent un
Vehiclefonctionnent aussi bien avec n'importe quelCar,MotorcycleouElectricCar. Consultez le polymorphisme Python pour une vue complète.