Une classe construite sur une autre
L'héritage te permet de définir une nouvelle classe comme l'extension d'une classe existante. La classe existante est le parent (aussi appelée classe de base ou superclasse), et la nouvelle est l'enfant (sous-classe). L'enfant reçoit toutes les méthodes et tous les attributs de classe du parent sans les répéter, et il peut ajouter les siens ou remplacer ceux qui doivent se comporter autrement.
Tu déclares le parent entre parenthèses après le nom de la classe :
Sortie :
Rex is an animal
Woof
Dog n'a ni __init__ ni describe à lui. Quand tu appelles Dog("Rex"), Python cherche __init__ sur Dog, ne le trouve pas, et utilise celui d'Animal. La même recherche a lieu pour rex.describe(). Pour rex.speak(), il trouve speak sur Dog en premier et s'arrête là.
Cet ordre de recherche est tout le mécanisme : Python cherche dans la classe de l'objet, puis dans le parent, puis dans le parent du parent, et utilise la première correspondance. Si les classes sont nouvelles pour toi, la page sur les classes présente d'abord __init__, self et les méthodes.
Redéfinir une méthode
Une méthode de l'enfant qui porte le même nom qu'une méthode du parent la remplace pour les instances de l'enfant. On appelle cela une redéfinition. Chaque sous-classe peut répondre au même appel à sa manière :
Sortie :
Rectangle with area 12
Square with area 25
Shape with area 0
summary n'est défini qu'une fois, sur Shape, mais il appelle self.area(). Comme self est l'objet réel, un Rectangle exécute Rectangle.area et un Square trouve area sur son parent Rectangle. Le code du parent appelle la méthode de l'enfant sans savoir que l'enfant existe. C'est ce qui rend l'héritage utile pour partager un déroulement commun tout en laissant chaque sous-classe remplir les détails.
Appeler le parent avec super()
Quand un enfant définit son propre __init__, le __init__ du parent ne s'exécute plus de lui-même. Si l'enfant a toujours besoin de l'initialisation du parent, appelle-la avec super().__init__() :
Sortie :
Ada 1050.0
super() renvoie un objet qui cherche les méthodes en partant du parent de la classe courante. super().__init__(owner, balance) exécute donc Account.__init__ sur le même objet, et self.owner et self.balance existent ensuite.
Oublie cette ligne et l'objet n'est construit qu'à moitié :
Sortie :
0.05
AttributeError: 'SavingsAccount' object has no attribute 'owner'
Tu peux écrire Account.__init__(self, owner) à la place, et le code ancien le fait souvent. super() est préférable parce qu'il ne répète pas le nom du parent et qu'il coopère correctement avec l'héritage multiple, présenté plus bas.
Étendre une méthode au lieu de la remplacer
super() fonctionne dans n'importe quelle méthode, pas seulement __init__. Appeler la version du parent à l'intérieur d'une redéfinition te permet d'ajouter du comportement autour d'elle au lieu de la réécrire :
Sortie :
started
[12:00] started
INFO [12:00] started
Chaque niveau ajoute un élément et délègue le reste vers le haut. Si le format du parent change plus tard, les deux enfants profitent du changement.
isinstance et issubclass
Une instance de sous-classe est aussi une instance de chaque classe au-dessus d'elle. isinstance(obj, cls) vérifie cette relation, et issubclass(child, parent) la vérifie entre deux classes :
Préfère isinstance à une comparaison directe de type(obj). Une vérification comme type(obj) is Animal rejette toutes les sous-classes, ce qui casse dès que quelqu'un ajoute Dog.
Toutes les classes que tu écris héritent de object, même quand tu ne le dis pas. class Animal: est identique à class Animal(object):. C'est de là que viennent les méthodes par défaut comme __str__, __repr__ et __eq__, et c'est pourquoi redéfinir __str__ change ce qu'affiche print() :
Sortie :
(2, 3)
True
Obliger les sous-classes à implémenter une méthode
Parfois, le parent ne doit jamais être utilisé directement, seulement comme modèle. Le module abc marque une telle classe comme abstraite. Toute méthode décorée avec @abstractmethod doit être redéfinie avant que la classe puisse être instanciée :
Les trois premières lignes de la sortie correspondent à l'export :
exporting 2 rows
1,2
3,4
La dernière ligne est une TypeError qui nomme Exporter et sa méthode export manquante (la formulation exacte varie selon les versions de Python). L'erreur apparaît à la création de l'objet, et non plus tard, quand export finit par être appelée.
Héritage multiple et MRO
Une classe peut indiquer plusieurs parents. Elle hérite alors de tous :
Sortie :
paddling flapping
swims
['Duck', 'Swimmer', 'Flyer', 'object']
Les deux parents définissent move, et Duck reçoit celle de Swimmer. Python tranche avec l'ordre de résolution des méthodes (MRO) : une liste unique de classes, stockée dans Duck.__mro__, que chaque recherche d'attribut parcourt du début à la fin. Elle commence par la classe elle-même, garde les parents dans l'ordre où tu les as écrits, place chaque classe avant ses propres parents, et se termine par object. Inverse l'ordre en class Duck(Flyer, Swimmer) et move renvoie "flies".
super() suit la même liste. Dans une méthode de Swimmer, super() signifie « la classe qui suit Swimmer dans le MRO de la classe de l'objet », ce qui, pour un Duck, est Flyer et non object. C'est ainsi que des classes coopératives peuvent chacune appeler super() et faire exécuter chaque classe de la chaîne exactement une fois, même dans un losange où deux parents partagent un grand-parent.
En pratique, l'héritage multiple sert surtout aux mixins : de petites classes qui ajoutent une seule capacité, comme un JsonMixin qui ajoute une méthode to_json, combinées à un parent principal. Les hiérarchies profondes avec plusieurs parents complets sont difficiles à suivre et rarement nécessaires.
Héritage ou composition ?
L'héritage modélise une relation « est un ». Un SavingsAccount est un Account : tout code qui fonctionne avec un Account doit fonctionner avec lui sans changement. Quand cette phrase sonne faux, c'est probablement la composition qu'il te faut, qui modélise « a un » : l'objet garde un autre objet comme attribut et l'appelle.
Sortie :
Roadster: 300 hp engine running
class Car(Engine) fonctionnerait aussi, mais cela affirmerait qu'une voiture est une sorte de moteur, donnerait à Car toutes les méthodes du moteur, et empêcherait de changer de moteur sans changer de classe. Avec la composition, les parties restent séparées, et remplacer le moteur revient à passer un autre objet.
Quelques signes que l'héritage n'est pas le bon outil :
- L'enfant redéfinit la plupart des méthodes du parent, ou en désactive certaines.
- Tu hérites uniquement pour réutiliser une ou deux méthodes utilitaires.
- La hiérarchie dépasse deux ou trois niveaux et tu dois lire plusieurs fichiers pour voir ce que fait une méthode.
Erreurs courantes
- Oublier
super().__init__(). Le__init__de l'enfant remplace celui du parent, donc les attributs du parent ne sont jamais définis et tu obtiens uneAttributeErrorplus tard. - Passer
selfaux appels desuper(). Écrissuper().__init__(name), passuper().__init__(self, name).super()lie déjà la méthode à l'objet courant. - Des signatures incompatibles lors d'une redéfinition. Si le
speak(self)du parent est appelé par du code partagé, unspeak(self, volume)de l'enfant avec un argument supplémentaire obligatoire casse ce code. Garde les redéfinitions compatibles, ou donne des valeurs par défaut aux nouveaux paramètres. - Vérifier
type(obj) == SomeClass. Cela ignore les sous-classes. Utiliseisinstance. - Utiliser l'héritage pour partager du code entre des classes sans rapport. Déplace plutôt le code partagé dans une fonction ou un objet utilitaire.
Questions fréquentes
Comment hériter d'une classe en Python ?
Mets la classe parente entre parenthèses après le nom de la nouvelle classe : class Dog(Animal):. Dog reçoit toutes les méthodes et tous les attributs de classe d'Animal, et peut en ajouter de nouveaux ou redéfinir ceux qui existent.
Que fait super().__init__() ?
Il appelle le __init__ de la classe parente pour que le parent puisse initialiser ses propres attributs. Quand une sous-classe définit son propre __init__, Python n'appelle pas automatiquement celui du parent : tu appelles donc super().__init__(...) avec les arguments dont le parent a besoin, puis tu définis les attributs supplémentaires de la sous-classe.
Quelle est la différence entre isinstance et type en Python ?
isinstance(obj, Animal) vaut True pour un Animal et pour les instances de n'importe quelle sous-classe d'Animal. type(obj) is Animal ne vaut True que si l'objet a été créé à partir d'Animal lui-même. Préfère isinstance, parce qu'il continue de fonctionner quand quelqu'un ajoute une sous-classe.
Python prend-il en charge l'héritage multiple ?
Oui : class C(A, B): hérite des deux. Quand les deux parents définissent la même méthode, Python en choisit une en suivant l'ordre de résolution des méthodes (MRO), que tu peux consulter avec C.__mro__. Il liste C en premier, puis ses parents de gauche à droite, et object en dernier.
Quand utiliser la composition plutôt que l'héritage ?
Utilise l'héritage pour une relation « est un », où la sous-classe peut servir partout où le parent est attendu (un SavingsAccount est un Account). Utilise la composition, c'est-à-dire stocker un objet comme attribut, pour une relation « a un » (une Car a un Engine). En cas de doute, la composition est plus facile à modifier par la suite.