L'héritage Python : Meilleures pratiques pour un code réutilisable
Imaginez que vous construisiez un système logiciel avec plusieurs rôles d'utilisateurs comme les étudiants, les enseignants et les administrateurs. Ces rôles partagent des attributs communs tels que le nom et l'identifiant, mais ils requièrent également des fonctionnalités qui leur sont propres. Au lieu de dupliquer le code, l'héritage vous permet de définir un comportement partagé dans une classe mère et de l'étendre dans des classes enfants spécialisées.
Dans cet article, nous allons explorer l'héritage Python, en abordant des concepts de base et avancés tels que la superposition de méthodes et la fonction super()
, qui est une fonction intégrée qui renvoie un objet temporaire de la superclasse, , afin que vous puissiez accéder à ses méthodes sans nommer explicitement la classe parente. Ne vous inquiétez pas si cela n'a pas encore de sens, car nous en apprendrons plus à ce sujet ci-dessous.
Apprenez Python à partir de zéro
Les bases de l'héritage en Python
L'héritage est l'un des piliers fondamentaux de la programmation orientée objet (POO) qui permet à une classe (appelée classe enfant) de dériver des attributs et des méthodes d'une autre classe (appelée classe mère). Cette caractéristique est essentielle pour laréutilisation du code sur le site et simplifie la maintenance, ce qui facilite la création de programmes évolutifs et efficaces.
Définition des classes parents et enfants
Avant d'aller plus loin, explorons la relation entre les classes parent et enfant.
Classe mère
Commençons par la classe mère. Une classe mère est la classe de base dont dérivent les classes enfants. Il encapsule les attributs et les méthodes partagés.
En utilisant Python, voici comment nous définissons une classe mère :
class ParentClass:
def __init__(self, attributes):
# Initialize attributes
pass
def method(self):
# Define behavior
pass
Classe d'enfants
Une classe enfant hérite des attributs et des méthodes de la classe parentale. Cela lui permet d'utiliser les fonctionnalités définies dans la classe mère. Le code suivant montre comment une classe enfant hérite des attributs et des méthodes d'une classe parentale :
class ChildClass(ParentClass):
def additional_method(self):
# Define new behavior
pass
Cette syntaxe simple permet à la classe enfant d'utiliser et d'étendre les fonctionnalités définies dans la classe parent.
Création d'une classe mère et d'une classe enfant
Créons un exemple pratique avec une classe Person
comme parent et une classe Student
comme enfant.
Création de la classe mère
La classe Person
contient des attributs partagés et une méthode d'affichage des informations :
# Defining the Parent Class
class Person:
def __init__(self, name, id):
self.name = name
self.id = id
def display_info(self):
return f"Name: {self.name}, ID: {self.id}"
Création de la classe enfantine
La classe Student
hérite de Person
et ajoute une nouvelle méthode study
.
# Defining the Child Class
class Student(Person):
def study(self):
return f"{self.name} is studying."
Testons les classes parent et enfant.
# Creating and Testing Instances
student = Student("Samuel", 102)
print(student.display_info())
print(student.study())
Name: Samuel, ID: 102
Samuel is studying.
Voici ce qui se passe :
-
La classe
Student
utilise la méthode__init__
dePerson
pour initialisername
etid
. -
La méthode
study
est propre à la classeStudent
, dont elle étend les fonctionnalités. -
La méthode
display_info
est héritée directement dePerson
.
Types d'héritage en Python
L'héritage en Python permet aux classes d'hériter des attributs et des comportements d'autres classes, ce qui favorise la réutilisation du code et une conception propre, comme nous l'avons évoqué précédemment. Dans cette section, nous pouvons parler des différents types d'héritage Python, qui comprennent l'héritage simple, multiple, hiérarchique et hybride en tant que catégories distinctes.
Héritage unique
On parle d'héritage unique lorsqu'une classe enfant hérite d'une seule classe parentale, ce qui lui permet d'étendre les fonctionnalités de la classe parentale. Ceci est utile lorsqu'un type d'objet partage des propriétés communes avec une catégorie plus large, mais nécessite également des attributs ou des comportements supplémentaires.
L'exemple que j'ai commencé à étudier tout à l'heure concernait l'héritage simple, mais examinons-le de plus près : Dans un système de gestion scolaire, tous les individus, y compris les élèves, les enseignants et le personnel, partagent des informations communes telles que name
et ID
. Cependant, les étudiants ont également des dossiers académiques tels que les notes et les cours inscrits. En utilisant l'héritage simple, nous pouvons créer une classe Person
pour les attributs partagés et l'étendre avec une classeStudent
pour les détails académiques.
Héritage unique. Image par l'auteur
Voici un bon exemple du scénario ci-dessus :
class Person:
"""Represents a general person with basic details."""
def __init__(self, name, id):
self.name = name
self.id = id
def get_details(self):
return f"Name: {self.name}, ID: {self.id}"
class Student(Person):
"""Represents a student, extending the Person class to include academic details."""
def __init__(self, name, id, grade, courses):
super().__init__(name, id)
self.grade = grade
self.courses = courses
def get_details(self):
return f"Name: {self.name}, ID: {self.id}, Grade: {self.grade}, Courses: {', '.join(self.courses)}"
# Example usage in a school system
student = Student("Samuel", 5678, "B+", ["Math", "Physics", "Computer Science"])
print(student.get_details())
Name: Samuel, ID: 5678, Grade: B+, Courses: Math, Physics, Computer Science
La classe Student
hérite de la méthode get_details()
de Person
mais l'étend pour inclure grade
et courses
. C'est un bon exemple de la façon dont l'héritage unique favorise ce que l'on appelle le code modulaire.
Héritage multiple
L'héritage multiple, comme un arbre généalogique, permet à une classe enfant d'hériter de plus d'une classe parentale, en combinant les attributs et les comportements de chacune d'elles. Cela peut entraîner des conflits potentiels, que Python résout à l'aide d'un ordre de résolution des méthodes (MRO).
Héritage multiple. Image par l'auteur
Jetez un coup d'œil :
class Person:
def get_details(self):
return "Details of a person."
class Athlete:
def get_skill(self):
return "Athletic skills."
class Student(Person, Athlete):
pass
# Example usage
student = Student()
print(student.get_details())
print(student.get_skill())
Details of a person.
Athletic skills.
Nous constatons que la classe Student
a hérité des attributs et des méthodes des classes Person
et Athlete
. Sans aucun effort supplémentaire, la classe Student
a accès à la méthode get_details()
de la classe mère Person
et à la méthode get_skill()
de la classe mère Athlete
. Nous combinons effectivement des fonctionnalités provenant de sources multiples.
Cependant, le fait d'hériter de plusieurs classes peut entraîner des conflits. Que se passe-t-il si les deux classes mères définissent une méthode ou un attribut portant le même nom ? J'ai parlé tout à l'heure de l'ordre de résolution des méthodes, mais permettez-moi d'en dire un peu plus à ce sujet. Ordre de résolution des méthodes détermine l'ordre dans lequel les classes sont recherchées pour les méthodes et les attributs. Le MRO suit une approche en profondeur, de gauche à droite.
Vous pouvez visualiser le MRO d'une classe en utilisant l'attribut __mro__
ou la méthode mro()
:
print(Student.__mro__)
(<class '__main__.Student'>, <class '__main__.Person'>, <class '__main__.Athlete'>, <class 'object'>)
Héritage multiniveau, hiérarchique et hybride
Python prend également en charge des structures d'héritage plus complexes. Je montrerai ces idées plus complexes en utilisant le même exemple.
Héritage à plusieurs niveaux
L'héritage à plusieurs niveaux se produit lorsqu'une classe enfant hérite d'une autre classe enfant, et que cette dernière hérite d'une classe parentale. Cela crée une chaîne d'héritage.
Héritage à plusieurs niveaux. Image par l'auteur
En voici un bon exemple :
class Person:
def __init__(self, name, id):
self.name = name
self.id = id
def get_details(self):
return f"Name: {self.name}, ID: {self.id}"
class Student(Person):
def __init__(self, name, id, grade):
super().__init__(name, id)
self.grade = grade
def get_details(self):
return f"Name: {self.name}, ID: {self.id}, Grade: {self.grade}"
class GraduateStudent(Student):
def __init__(self, name, id, grade, thesis_title):
super().__init__(name, id, grade)
self.thesis_title = thesis_title
def get_details(self):
return f"Name: {self.name}, ID: {self.id}, Grade: {self.grade}, Thesis: {self.thesis_title}"
# Example usage
grad_student = GraduateStudent("Charlie", 91011, "A", "AI in Healthcare")
print(grad_student.get_details())
Name: Charlie, ID: 91011, Grade: A, Thesis: AI in Healthcare
Ici, chaque classe de la chaîne ajoute quelque chose de nouveau : Person
gère les noms et les identifiants, Student
inclut les notes et GraduateStudent
introduit une thèse. Grâce à super().__init__()
, nous réutilisons la logique d'initialisation sans dupliquer le code. C'est efficace, soigné et cela garantit que chaque niveau de "l'échelle de l'héritage", comme je l'entends, fonctionne.
Héritage hiérarchique
Dans l'héritage hiérarchique, plusieurs classes enfants héritent d'une seule classe parentale, ce qui permet de partager le comportement de sous-classes dotées d'attributs uniques.
Héritage hiérarchique. Image par l'auteur
Voyons ensemble un bon exemple :
class Person:
def __init__(self, name, id):
self.name = name
self.id = id
def get_details(self):
return f"Name: {self.name}, ID: {self.id}"
class Student(Person):
def __init__(self, name, id, grade):
super().__init__(name, id)
self.grade = grade
def get_details(self):
return f"Name: {self.name}, ID: {self.id}, Grade: {self.grade}"
class Teacher(Person):
def __init__(self, name, id, subject):
super().__init__(name, id)
self.subject = subject
def get_details(self):
return f"Name: {self.name}, ID: {self.id}, Subject: {self.subject}"
# Example usage
student = Student("Samuel", 5678, "B+")
teacher = Teacher("Dr. Smith", 1234, "Math")
print(student.get_details())
print(teacher.get_details())
Name: Samuel, ID: 5678, Grade: B+
Dr. Smith, ID: 1234, Subject: Math
Ici, la classe Person
sert de base, offrant des attributs et des méthodes communs (name
, id
, et get_details
). Les classes Student
et Teacher
étendent ensuite cette fonctionnalité en ajoutant leurs propres propriétés (grade
et subject
) et en personnalisant la méthode get_details
pour refléter leurs contextes spécifiques.
Avec cette approche, les fonctionnalités partagées restent en un seul endroit (la classe Person
), tandis que les comportements spécialisés sont soigneusement encapsulés dans les sous-classes.
Héritage hybride
L'héritage hybride combine plusieurs types d'héritage, tels que l'héritage à plusieurs niveaux ou l'héritage multiple, pour modéliser des relations plus complexes.
Héritage hybride. Image par l'auteur
Prenons un exemple qui illustre la complexité de l'héritage hybride.
# Base class
class Person:
def __init__(self, name, id):
self.name = name
self.id = id
def get_details(self):
return f"Name: {self.name}, ID: {self.id}"
# Intermediate class inheriting from the base class
class Employee(Person):
def __init__(self, name, id, position):
super().__init__(name, id)
self.position = position
def get_position(self):
return f"Position: {self.position}"
# Another independent base class
class Athlete:
def __init__(self, sport):
self.sport = sport
def get_sport(self):
return f"Sport: {self.sport}"
# Derived class combining Employee and Athlete
class Student(Employee, Athlete):
def __init__(self, name, id, position, grade, sport):
Employee.__init__(self, name, id, position)
Athlete.__init__(self, sport)
self.grade = grade
def get_grade(self):
return f"Grade: {self.grade}"
# Example usage
student = Student("Samuel", 1234, "Intern", "A", "Soccer")
print(student.get_details()) # From Person
print(student.get_position()) # From Employee
print(student.get_grade()) # From Student
print(student.get_sport()) # From Athlete
Name: Samuel, ID: 1234
Position: Intern
Grade: A
Sport: Soccer
Dans cet exemple, la classe Student
démontre l'héritage hybride en héritant des attributs et des méthodes de Employee
(qui hérite elle-même de Person
) et de Athlete
. Il combine l'héritage hiérarchique de (où Employee
hérite de Person
) et l'héritage multiple (où Student
hérite à la fois de Employee
et de Athlete
).
Avantages de l'héritage en Python
Il est maintenant temps de voir les forces et les faiblesses :
Avantages de l'héritage
-
Réutilisation : Avec l'héritage, vous pouvez écrire du code une fois dans la classe mère et le réutiliser dans les classes enfants. Si l'on reprend l'exemple,
FullTimeEmployee
etContractor
peuvent hériter d'une méthodeget_details()
de la classe mèreEmployee
. -
Simplicité : L'héritage permet de modéliser clairement les relations. Un bon exemple est la classe
FullTimeEmployee
qui est un type de la classe parentaleEmployee
. -
Évolutivité : Il permet également d'ajouter de nouvelles fonctionnalités ou classes enfantines sans affecter le code existant. Par exemple, nous pouvons facilement ajouter une nouvelle classe
Intern
en tant que classe enfant.
Limites potentielles de l'héritage
-
La complexité : Cela ne vous surprendra pas, mais trop de niveaux d'héritage peuvent rendre le code difficile à suivre. Par exemple, si une classe
Employee
a trop de classes enfants commeManager
,Engineer
,Intern
, etc., elle peut devenir confuse. -
Dépendance : Les modifications apportées à une classe mère peuvent affecter involontairement toutes les sous-classes. Si vous modifiez
Employee
par exemple, vous risquez de casserFullTimeEmployee
ouContractor
. -
Mauvais usage : L'utilisation de l'héritage lorsqu'il n'est pas le mieux adapté peut compliquer les conceptions. Vous ne voudriez pas créer une solution où
Car
hérite deBoat
juste pour réutilisermove()
. La relation n'a pas de sens.
Techniques avancées d'héritage en Python
Maintenant que nous avons exploré les bases de l'héritage, examinons quelques techniques avancées. Ces techniques, telles que la superposition de méthodes, super()
, les classes de base abstraites et le polymorphisme, améliorent la flexibilité du code et permettent des schémas de conception plus sophistiqués.
Surcharge de méthodes en Python
La surcharge de méthode permet à une classe enfant de fournir une implémentation spécifique pour une méthode déjà définie dans sa classe mère. Ceci est utile lorsque le comportement hérité ne répond pas entièrement aux exigences de la classe enfantine.
class Person:
def __init__(self, name, id):
self.name = name
self.id = id
def get_details(self):
return f"Name: {self.name}, ID: {self.id}"
class Student(Person):
def __init__(self, name, id, grade):
super().__init__(name, id)
self.grade = grade
# Overriding the get_details method
def get_details(self):
return f"Name: {self.name}, ID: {self.id}, Grade: {self.grade}"
# Example usage
student = Student("Samuel", 1234, "A")
print(student.get_details())
Name: Samuel, ID: 1234, Grade: A
Ici, la classe Student
remplace la méthode get_details()
de la classe Person
pour donner ses propres implémentations spécifiques. Cela permet à la classe enfant d'avoir son propre comportement tout en suivant le même nom de méthode.
Alors pourquoi passer outre ? Nous surchargeons parce que nous voulons personnaliser le comportement hérité et aussi parce que nous voulons adapter la fonctionnalité d'une méthode parent aux exigences uniques d'une classe enfant.
Utilisation de super() pour l'initialisation du parent
La fonction super()
est utilisée pour appeler les méthodes de la classe mère à partir de la classe enfant. Ceci est particulièrement utile lorsque vous souhaitez étendre ou modifier la fonctionnalité d'une méthode de la classe parente, telle que la méthode du constructeur __Init__()
.
Pourquoi utiliser la fonction super()
? Nous utilisons la fonction super parce que nous voulons appeler et initialiser le constructeur de la classe parente et aussi parce que nous voulons éviter de nommer explicitement la classe parente. Cela est utile, en particulier dans les cas d'héritages multiples.
class Person:
def __init__(self, name, id):
self.name = name
self.id = id
class Student(Person):
def __init__(self, name, id, grade):
# Using super() to initialize the parent class
super().__init__(name, id)
self.grade = grade
# Example usage
student = Student("Samuel", 5678, "B+")
print(student.name)
print(student.id)
print(student.grade)
Samuel
5678
B+
Ici, la classe Student
utilise super().__init__(name, id)
pour appeler la méthode __init__
de la classe parente Person
, de sorte qu'il n'est pas nécessaire de répéter le code pour initialiser les attributs name
et id
. La classe enfant introduit ensuite un attribut grade
, qui est spécifique à la classe Student
.
Classes de base abstraites (ABC)
Une classe de base abstraite (ABC) est une classe qui ne peut pas être utilisée directement pour créer des objets. Il a pour but de définir un ensemble commun de méthodes que d'autres classes doivent mettre en œuvre. Les ABC sont donc utiles lorsque vous voulez vous assurer que certaines méthodes sont toujours présentes dans les classes enfantines.
from abc import ABC, abstractmethod
class Person(ABC):
@abstractmethod
def get_details(self):
pass
class Student(Person):
def __init__(self, name, id, grade):
self.name = name
self.id = id
self.grade = grade
def get_details(self):
return f"Name: {self.name}, ID: {self.id}, Grade: {self.grade}"
# Example usage
student = Student("Hamilton", 7890, "A-")
print(student.get_details())
Name: Hamilton, ID: 7890, Grade: A-
La classe Person
est une classe abstraite qui exige que toute classe enfant implémente la méthode get_details()
. Cette méthode est ensuite mise en œuvre par la classe enfant, Student
.
Polymorphisme
Le polymorphisme est synonyme de formes multiples. En Python, cela permet à différentes classes d'utiliser le même nom de méthode, mais chacune peut implémenter cette méthode d'une manière différente.
Le polymorphisme nous aide à écrire un code qui peut fonctionner avec des objets de différentes classes, même si ces classes ont des comportements différents :
class Person:
def get_details(self):
return "Details of a person."
class Student(Person):
def get_details(self):
return "Details of a student."
class Teacher(Person):
def get_details(self):
return "Details of a teacher."
# Example usage
def print_details(person):
print(person.get_details())
student = Student()
teacher = Teacher()
print_details(student)
print_details(teacher)
Details of a student.
Details of a teacher.
Dans cet exemple, la fonction print_details()
peut accepter n'importe quel objet du type Person
, mais elle appellera la méthode get_details()
appropriée selon que l'objet est un Student
ou un Teacher
.
Erreurs courantes et meilleures pratiques
Bien que l'héritage soit puissant, il est facile de l'utiliser à mauvais escient. Je vais partager avec vous quelques idées pour vous aider à en tirer le meilleur parti.
Éviter les surprises avec les méthodes surchargées
Lorsqu'une classe enfant remplace une méthode de sa classe mère, le comportement peut changer.
Par exemple, si la classe mère Employee
possède une méthode calculate_pay()
et que la classe enfant Manager
la remplace sans tenir compte de tous les scénarios, les calculs de rémunération risquent d'être incorrects.
Dans ce cas, la meilleure pratique consiste à toujours tester minutieusement les méthodes remplacées et à documenter leur comportement.
Choisir entre l'héritage et la composition
Je sais que cet article traite de l'héritage, mais ce n'est pas toujours la bonne approche. Parfois, la composition, qui consiste à créer des classes en combinant des objets plutôt qu'en les étendant, peut être mieux adaptée à ce que vous faites.
Pour schématiser les différences de la manière la plus élémentaire, pensez que.. :
-
L'héritage fait référence à relations "Is-a". Par exemple, un
Manager
est unEmployee
. -
La composition fait référence à relations "Has-a". Par exemple, un
Car
a unEngine
.
Alors, comment savoir si la composition est la meilleure approche à adopter ? Utilisez la composition lorsquela relation n'est pas strictement hiérarchique et/ou lorsque vous souhaitezréduire le couplage étroit entre les classes.
Nous pourrions également dire que, tandis que l'héritage modélise les relations, la composition se concentre sur la fonctionnalité. Pour vous aider, considérez ceci :
-
Utilisez l'héritage lorsque les objets sont naturellement hiérarchisés. Par exemple,
Animal > Bird > Parrot
. -
Utilisez la composition lorsque des objets partagent des fonctionnalités mais ne sont pas liés. Par exemple, les sites
Printer
etScanner
utilisent tous deux le siteDeviceManager
.
Éviter les chaînes d'héritage profondes
Les chaînes d'héritage profondes (plusieurs niveaux de relations parent-enfant) peuvent rendre votre code difficile à lire et à maintenir. C'est un problème car les changements apportés à une classe mère peuvent affecter involontairement de nombreuses classes enfants. En outre, d ebuggingdevient complexe lorsque le comportement est réparti sur plusieurs niveaux.
Dans ce cas, la meilleure pratique consiste à kgarder les hiérarchies peu profondes. De même, consider à utiliser la composition (comme je l'ai mentionné plus haut) ou à diviser une chaîne en hiérarchies distinctes si vous trouvez qu'elle devient trop profonde.
Conclusion
L'héritage est un pilier majeur de la programmation orientée objet qui permet aux développeurs comme vous de créer un code réutilisable, modulaire et évolutif. Si vous maîtrisez l'héritage, vous pourrez facilement simplifier des systèmes complexes.
Un bon moyen d'approfondir votre compréhension est d'essayer de construire des structures d'héritage dans vos projets. Commencez simplement, puis expérimentez des hiérarchies plus complexes pour voir comment elles fonctionnent dans la pratique.
Si vous souhaitez aller encore plus loin, vous pouvez consulter notre cours Concepts du paradigme de programmation pour une compréhension plus approfondie de l'héritage et d'autres idées. Notre cursus professionnel de développeur Python est également une bonne ressource qui offre un parcours complet pour développer des compétences de programmation avancées qui vous équiperont pour le développement de logiciels.
Professionnel expérimenté des données et écrivain passionné par l'autonomisation des experts en herbe dans le domaine des données.
FAQ sur l'héritage Python
Qu'est-ce que l'héritage en Python ?
En Python, l'héritage est un mécanisme qui permet à une classe d'hériter des attributs et des méthodes d'une autre classe, ce qui favorise la réutilisation du code et les structures de classe hiérarchiques.
Comment fonctionne l'héritage Python ?
L'héritage Python consiste à définir une nouvelle classe qui hérite des attributs et des méthodes d'une classe existante, ce qui permet de créer des structures de classe hiérarchiques.
Quels sont les types d'héritage en Python ?
Python prend en charge plusieurs types d'héritage, notamment l'héritage simple, multiple, multiniveau et hiérarchique.
Quelle est la différence entre l'héritage simple et l'héritage multiple en Python ?
L'héritage simple ne concerne qu'une seule classe mère, tandis que l'héritage multiple permet à une classe d'hériter de plusieurs classes mères.
Comment implémenter l'héritage en Python ?
Mettez en œuvre l'héritage en définissant une nouvelle classe qui spécifie une classe mère dans sa définition, à l'aide de la syntaxe class ChildClass(ParentClass):
.
Quels sont les avantages de l'héritage en Python ?
L'héritage favorise la réutilisation du code, permet la création de structures de classes hiérarchiques et prend en charge le polymorphisme, ce qui rend le code plus flexible et plus facile à maintenir.
Comment implémenter l'héritage multiple en Python ?
L'héritage multiple est mis en œuvre en définissant une classe qui hérite de plusieurs classes de base, Python gérant la résolution des méthodes en utilisant l'ordre de résolution des méthodes (MRO).