Héritage et polymorphisme¶
Une application Python est composée d’un ensemble d’objets. Un des intérêts de la programmation orientée objet (POO) réside dans les relations que ces objets entretiennent les uns avec les autres. Ces relations sont construites par les développeurs et constituent ce que l’on appelle l’architecture d’une application. Il existe deux relations fondamentales en POO :
- a un(e) (has-a)
Cette relation permet de créer une relation de dépendance d’une classe envers une autre. Une classe a besoin des services d’une autre classe pour réaliser sa fonction. On parle également de relation de composition pour désigner ce type de relation.
La relation « a un » implique simplement qu’un objet possède un attribut qui référence un autre objet. Par exemple, si une programme déclare une classe
Personne
, cette classe peut avoir une propriété pour stocker l’adresse sous la forme d’une classe adresse :1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
class Personne: def __init__(self, nom, prenom, adresse): self.nom = nom self.prenom = prenom self.adresse = adresse class Adresse: def __init__(self, rue, code_postal, ville): self.rue = rue self.code_postal = code_postal self.ville = ville adresse = Adresse(rue="4 rue d'ici", code_postal="78000", ville="Paris") personne = Personne(prenom="Jean", nom="Dumond", adresse=adresse)
Le code ci-dessus illustre la relation de composition et on peut bien dire qu’une personne a une adresse.
- est un(e) (is-a)
Cette relation permet de créer une chaîne de relation d’identité entre des classes. Elle indique qu’une classe peut être assimilée à une autre classe qui correspond à une notion plus abstraite ou plus générale. On parle d’héritage pour désigner le mécanisme qui permet d’implémenter ce type de relation. Le reste de ce chapitre est consacré à ce type particulier de relation entre classes.
L’héritage¶
L’héritage est donc le mécanisme qui permet de traduire une relation de type « est un(e) ».
Prenons l’exemple d’une classe Voiture
et d’une classe Vehicule
. Il est
possible de dire qu’une voiture est un véhicule. Je peux traduire cette
relation en Python. On dit que Vehicule
est la classe parente de Voiture
ou la généralisation. Symétriquement, on dit que Voiture
est la classe
enfant (ou fille) de Vehicule
ou la spécialisation. On dit aussi que
la classe Vehicule
est la super classe de Voiture
.
class Vehicule:
pass
class Voiture(Vehicule):
pass
Cette relation se traduit en précisant le nom de la classe parente entre parenthèses après le nom de la classe.
Si nous créons un objet à partir de la classe Voiture
, cet objet est à la
fois de type Vehicule
et de type Voiture
.
>>> v = Voiture()
>>> isinstance(v, Voiture)
True
>>> isinstance(v, Vehicule)
True
Le mot de héritage vient du fait qu’une classe fille hérite des attributs et des comportements de sa classe mère.
class Vehicule:
def __init__(self):
self._vitesse = 0
@property
def vitesse(self):
return self._vitesse
def accelerer(self, delta_vitesse):
self._vitesse += delta_vitesse
def decelerer(self, delta_vitesse):
self._vitesse -= delta_vitesse
class Voiture(Vehicule):
pass
>>> v = Voiture()
>>> v.vitesse
0
>>> v.accelerer(80)
>>> v.vitesse
80
Les objets de type Voiture
disposent des mêmes comportements, propriétés et
attributs déclarés dans la classe Vehicule
.
La classe fille peut fournir ses propres comportements et ses propres attributs :
class Voiture(Vehicule):
def klaxonner(self):
print("tût tût !")
Constructeur et héritage¶
Le constructeur est, comme toutes les méthodes, hérité. Cependant que
ce passe-t-il si nous souhaitons ajouter une constructeur dans notre classe
Voiture
?
class Voiture(Vehicule):
def __init__(self, klaxon="tût tût !"):
self.klaxon = klaxon
def klaxonner(self):
print(self.klaxon)
>>> v = Voiture()
>>> v.klaxonner()
tût tût !
>>> v.vitesse
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
return self._vitesse
AttributeError: 'Voiture' object has no attribute '_vitesse'
Cela ne fonctionne plus car le constructeur déclaré dans la classe Voiture
remplace et cache celui hérité de la classe Vehicule
et l’initialisation
de l’attribut _vitesse
n’est plus faite. Quand une classe hérite d’une autre
classe, elle a la responsabilité de s’assurer que le constructeur de la classe
parente est appelé pour garantir l’initialisation. Pour cela, Python 3 fournit
la fonction super()
qui retourne le type de la super classe. L’implémentation
correcte de la classe Voiture
doit donc être :
class Voiture(Vehicule):
def __init__(self, klaxon="tût tût !"):
super().__init__()
self.klaxon = klaxon
def klaxonner(self):
print(self.klaxon)
>>> v = Voiture()
>>> v.klaxonner()
tût tût !
>>> v.vitesse
0
Il est donc facile de passer des paramètres à l’initialisation de la classe parente si cette dernière attend des paramètres de constructeur.
class Vehicule:
def __init__(self, marque=None, vitesse_initiale=0):
self.marque = marque
self._vitesse = vitesse_initiale
@property
def vitesse(self):
return self._vitesse
def accelerer(self, delta_vitesse):
self._vitesse += delta_vitesse
def decelerer(self, delta_vitesse):
self._vitesse -= delta_vitesse
class Voiture(Vehicule):
def __init__(self, marque=None, vitesse_initiale=0, klaxon="tût tût !"):
super().__init__(marque, vitesse_initiale)
self.klaxon = klaxon
def klaxonner(self):
print(self.klaxon)
>>> v = Voiture("De Lorean", 88.0)
>>> v.vitesse
88.0
>>> v.marque
'De Lorean'
>>> v.klaxonner()
tût tût !
Note
Pour chaîner l’appel des constructeurs, il est également possible d’appeler directement le constructeur en désignant la classe parente :
class Voiture(Vehicule):
def __init__(self, marque=None, vitesse_initiale=0, klaxon="tût tût !"):
Vehicule.__init__(marque, vitesse_initiale)
self.klaxon = klaxon
Cette écriture est cependant considérée comme obsolète en Python 3 grâce à
la fonction super()
.
Héritage et mutualisation de code¶
Un des intérêts de l’héritage est de permettre la réutilisation de code. Maintenant
que nous avons défini une Vehicule
, nous pouvons imaginer diverses classes
appropriées pour notre système et qui sont des véhicules.
class Charette(Vehicule):
def __init__(self, vitesse_initiale=0):
super().__init__(vitesse_initiale=vitesse_initiale)
>>> c = Charette(1)
>>> c.accelerer(5)
>>> c.vitesse
6
La classe object¶
Python définit la classe object
. Toutes les classes hérite directement
ou indirectement de cette classe. Si une classe ne déclare aucune classe parente
alors sa classe parente est object
.
class MaClasse:
pass
La déclaration ci-dessus est strictement équivalente à :
class MaClasse(object):
pass
Il est possible de créer des instances de la classe object
. Cela
reste d’un usage limité car cette classe n’offre aucune méthode particulière et
il n’est pas possible d’ajouter dynamiquement des attributs à ses instances.
>>> o = object()
>>> o.nom = "Un nom"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'object' object has no attribute 'nom'
Le polymorphisme¶
Le polymorphisme est un mécanisme important dans la programmation objet. Il permet de modifier le comportement d’une classe fille par rapport à sa classe mère. Le polymorphisme permet d’utiliser l’héritage comme un mécanisme d’extension en adaptant le comportement des objets.
Prenons l’exemple de la classe Animal
. Cette classe offre une méthode
crier
. Pour simplifier notre exemple, la méthode se contente d’écrire
le cri de l’animal sur la sortie standard.
class Animal:
def crier(self):
print("un cri d'animal")
Nous pouvons créer les classes Chien
et Chat
qui héritent toutes deux
de la classe Animal
. Ces classes peuvent être des spécialisations de la
classe Animal
en ce qu’elles peuvent redéfinir (override) le comportement
de la méthode crier ()
.
class Chien(Animal):
def crier(self):
print("whouaf whouaf !")
class Chat(Animal):
def crier(self):
print("miaou !")
>>> a = Animal()
>>> animal = Animal()
>>> animal.crier()
un cri d'animal
>>> animal = Chien()
>>> animal.crier()
whouaf whouaf !
>>> animal = Chat()
>>> animal.crier()
miaou !
Un chat est bien un animal, un chien est bien un animal. Les objets de ces types
disposent bien de la méthode crier()
mais son comportement est polymorphe.
Il dépend du type réel de l’objet.
À noter qu’il est toujours possible d’appeler la méthode parente dans la classe
enfant grâce à la fonction super()
.
class Chien(Animal):
def crier(self):
print("whouaf whouaf !")
def crier_comme_un_animal(self):
super().crier()
>>> chien = Chien()
>>> chien.crier()
whouaf whouaf !
>>> chien.crier_comme_un_animal()
un cri d'animal
Polymorphisme et duck typing¶
Pour les langages de programmation à typage fort comme le C++ ou le Java, le polymorphisme a des implications très fortes et il permet d’introduire une grande souplesse dans les relations entre types. Pour les langages de programmation à typage dynamique comme Python, le polymorphisme, même s’il reste une notion importante, a moins d’impact.
En Python, on préfère souvent le principe du duck typing (ou typage canard). Cette approche repose sur le principe suivant :
« Si je vois un oiseau qui vole comme un canard, cancane comme un canard, etnage comme un canard, alors j’appelle cet oiseau un canard. »
Cela signifie que le type réel est moins important que le comportement attendu.
Pour reprendre notre exemple des véhicules : si une classe offre des méthodes
pour accélérer, décélérer et des propriétés pour la vitesse et la marque, alors
c’est qu’il s’agit d’un véhicule et peu importe que cette classe hérite ou
non de la classe Vehicule
. Tous les objets de cette classe pourront être
utilisés dans un contexte qui manipule des véhicules.
Masquer les attributs et les méthodes pour les classe filles¶
Parfois, on ne souhaite pas qu’une méthode puisse être redéfinie ou qu’un attribut puisse être modifié dans une classe fille. Pour cela, il suffit que le nom de la méthode ou de l’attribut commence par deux caractères soulignés (underscores). Nous avons vu précédemment que Python n’a pas de mécanisme pour contrôler la visibilité des éléments d’une classe. Par convention, les développeurs signalent par un caractère souligné (underscore) le statut privé d’un attribut ou d’une méthode. Par contre le recours à deux caractères soulignés à un impact sur l’interpréteur. Ce dernier renomme la méthode ou l’attribut de la forme :
_<nom de la classe>__<nom>
Ainsi si nous modifions le nom de la la classe Vehicule
:
class Vehicule:
def __init__(self, marque=None, vitesse_initiale=0):
self.marque = marque
self.__vitesse = vitesse_initiale
@property
def vitesse(self):
return self.__vitesse
def accelerer(self, delta_vitesse):
self.__vitesse += delta_vitesse
def decelerer(self, delta_vitesse):
self.__vitesse -= delta_vitesse
L’attribut __vitesse
sera renommé _Vehicule__vitesse
. Donc, si un attribut
__vitesse
est également utilisé dans une classe fille, l’interpréteur
considérera qu’il s’agit d’un attribut différent.
Note
Cette notation est avant tout faite pour éviter des effets de bord involontaire lorsqu’on hérite d’une classe dont on ne connaît pas bien le code source. Après tout, il est toujours possible d’implémenter une classe fille qui utilise un attribut ou déclare une méthode sans que le développeur ait conscience qu’il réutilise un attribut ou qu’il redéfinit une méthode d’une classe parente.
Les exceptions comme objets¶
En Python, une exception est un objet qui est directement ou indirectement
une instance de la classe BaseException
.
Il existe beaucoup de classes pour représenter des exceptions en Python.
Vous pouvez consulter la documentation pour connaître la
hiérarchie des exceptions. Néanmoins, il est très simple de créer ses
propres exceptions. Il est recommandé de créer des exceptions en héritant de
Exception
ou d’une classe héritant de Exception
.
Exception
est une classe qui hérite de BaseException
.
Pour simplifier l’implémentation, BaseException
définit l’attribut
args
qui contient tous les paramètres passés au
constructeur.
class MonException(Exception):
pass
try:
raise MonException("Mon message")
except MonException as e:
print(e.args)
# Affiche ('Mon message',)
print(e)
# Affiche Mon message
Héritage multiple et mixin¶
Une classe peut hériter de plusieurs classes. Dans ce cas, elle héritera des méthodes et des attributs de l’ensemble de ces classes et de leurs classes parentes. Il suffit d’indiquer la liste des classes parentes en les séparant par une virgule.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class Animal:
def crier(self):
pass
class Carnivore:
def chasser(self):
pass
class Chien(Animal, Carnivore):
"""Un chien qui est à la fois un animal et un carnivore"""
|
>>> c = Chien()
>>> c.chasser()
>>> c.crier()
La classe Chien
hérite des comportements mais aussi des attributs des classes
parentes. La variable c
désigne un objet qui est à la fois un Chien
, un
Animal
et un Carnivore
.
>>> isinstance(c, Chien)
True
>>> isinstance(c, Animal)
True
>>> isinstance(c, Carnivore)
True
L’héritage en diamant¶
Il peut arriver qu’une classe hérite indirectement plusieurs fois de la même
classe. Reprenons notre exemple précédent et supposons que les classes Animal
et Carnivore
héritent toutes deux de la classe EtreVivant
et que la
classe EtreVivant
possède l’attribut point_de_vie
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class EtreVivant:
def __init__(self):
self.point_de_vie = 100
class Animal(EtreVivant):
def dormir(self):
self.point_de_vie += 1
class Carnivore(EtreVivant):
def chasser(self):
self.point_de_vie -= 1
class Chien(Animal, Carnivore):
"""Un chien qui est à la fois un animal et un carnivore"""
|
Dans ce cas, la classe Chien
hérite deux fois de la classe EtreVivant
.
À cause de la représentation graphique d’une telle situation, on appelle
ce cas particulier l’héritage en diamant.
Python résout cette situation en considérant qu’une classe ne peut pas directement
ou indirectement hériter plusieurs fois d’une même classe. Donc, les instances
de la classe Chien
ne posséderont qu’un seul attribut point_de_vie
.
>>> c = Chien()
>>> c.chasser()
>>> c.point_de_vie
99
>>> c.dormir()
>>> c.point_de_vie
100
Mais que se passe-t-il si l’héritage en diamant implique des méthodes ? Par exemple,
imaginons que la classe EtreVivant
possède la méthode se_nourrir()
qui
est redéfinie à la fois dans la classe Animal
et dans la classe Carnivore
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | class EtreVivant:
def __init__(self):
self.point_de_vie = 100
def se_nourrir(self):
self.point_de_vie += 1
class Animal(EtreVivant):
def dormir(self):
self.point_de_vie += 1
def se_nourrir(self):
self.point_de_vie += 5
class Carnivore(EtreVivant):
def chasser(self):
self.point_de_vie -= 1
def se_nourrir(self):
self.point_de_vie += 10
class Chien(Animal, Carnivore):
"""Un chien qui est à la fois un animal et un carnivore"""
|
Que se passe-t-il si nous appelons la méthode se_nourrir()
depuis une instance
de Chien
?
>>> c = Chien()
>>> c.se_nourrir()
>>> c.point_de_vie
105
C’est la méthode de la classe Animal
qui est exécutée, passant la valeur
de point_de_vie
à 105. Pour choisir quelle méthode est appelée, l’interpréteur
se base sur un algorithme appelé method resolution order ou mro. Il
s’agit de chercher dans la liste de l’héritage des classes la première déclaration
de la méthode appelée. On peut facilement connaître le mro d’une classe car
il existe la méthode class.mro()
qui retourne précisément cette liste.
Pour accéder à la classe d’une instance, on peut utiliser la fonction type
.
>>> type(c).mro()
[<class '__main__.Chien'>, <class '__main__.Animal'>,
<class '__main__.Carnivore'>, <class '__main__.EtreVivant'>,
<class 'object'>]
On voit que pour le type Chien
, un appel à une méthode conduit à chercher
cette méthode d’abord dans la classe Chien
et ensuite dans les classes Animal
Carnivore
, EtreVivant
et enfin object
.
Note
Rappelez-vous qu’une classe qui ne précise explicitement aucune classe parente
hérite tout de même de la classe object
.
Si nous intervertissons l’ordre d’héritage de la déclaration de la classe
Chien
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | class EtreVivant:
def __init__(self):
self.point_de_vie = 100
def se_nourrir(self):
self.point_de_vie += 1
class Animal(EtreVivant):
def dormir(self):
self.point_de_vie += 1
def se_nourrir(self):
self.point_de_vie += 5
class Carnivore(EtreVivant):
def chasser(self):
self.point_de_vie -= 1
def se_nourrir(self):
self.point_de_vie += 10
class Chien(Carnivore, Animal):
"""Un chien qui est à la fois un animal et un carnivore"""
|
>>> c = Chien()
>>> c.se_nourrir()
>>> c.point_de_vie
110
>>> type(c).mro()
[<class '__main__.Chien'>, <class '__main__.Carnivore'>,
<class '__main__.Animal'>, <class '__main__.EtreVivant'>,
<class 'object'>]
Maintenant, c’est la méthode se_nourrir()
de la classe Carnivore
qui est appelée pour un objet de type Chien
.
L’ordre de déclaration de l’héritage multiple est donc primordial !
Appel des constructeurs¶
Si nous voulons déclarer des constructeurs, il faut impérativement appeler le constructeur de la super classe.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | class EtreVivant:
def __init__(self, point_de_vie):
self.point_de_vie = point_de_vie
def se_nourrir(self):
self.point_de_vie += 1
class Animal(EtreVivant):
def __init__(self, nom, point_de_vie):
super().__init__(point_de_vie)
self.nom = nom
def dormir(self):
self.point_de_vie += 1
def se_nourrir(self):
self.point_de_vie += 5
class Carnivore(EtreVivant):
def chasser(self):
self.point_de_vie -= 1
def se_nourrir(self):
self.point_de_vie += 10
class Chien(Carnivore, Animal):
"""Un chien qui est à la fois un animal et un carnivore"""
def __init__(self, point_de_vie, nom):
super().__init__(point_de_vie, nom)
|
>>> c = Chien("Médor", 60)
>>> c.nom
'Médor'
>>> c.point_de_vie
60
>>> c.se_nourrir()
>>> c.point_de_vie
70
La fonction super()
retourne le type suivant dans la liste du
mro. La classe Carnivore
ne proposant pas de constructeur, elle se contente
de passer l’appel au constructeur de la classe Animal
qui est la classe
suivante dans la liste mro.
Les mixins¶
Un mixin est une classe qui permet d’ajouter des fonctionnalités supplémentaires. Il s’agit simplement d’une classe comme une autre mais qui n’est pas destinée à être utilisée directement pour créer des instances.
Imaginons que nous désirions ajouter à certaines de nos classes la possibilité
d’afficher leur état (la valeur des attributs) grâce à une méthode afficher()
.
Nous pouvons créer un mixin proposant cette méthode. Ce mixin accepte
en paramètre le caractère à utiliser comme séparateur ainsi que l’indentation
à utiliser lors de l’affichage.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | class Affichable:
def __init__(self, *args, indentation=0, separateur="-", **kwargs):
self.__indentation = indentation
self.__separateur = separateur
super().__init__(*args, **kwargs)
def afficher(self):
cls = type(self)
separateur = self.__separateur * 80
print(separateur)
print("Objet de la classe", cls.__name__)
print(cls.__doc__)
print("mro : ", cls.mro())
print()
indentation = ' ' * self.__indentation
for attr in self.__dict__:
if not attr.startswith('_'):
valeur = getattr(self, attr)
print(f"{indentation}{attr} = {valeur}")
print(separateur)
|
Nous pouvons modifier notre implémentation de la classe Chien
pour la rendre
affichable :
1 2 3 4 5 | class Chien(Affichable, Animal):
"""Un chien qui est à la fois un animal et un carnivore"""
def __init__(self, point_de_vie, nom, **kwargs):
super().__init__(point_de_vie, nom, **kwargs)
|
Ainsi nous pouvons afficher l’état interne d’un objet de type Chien
.
>>> c = Chien(40, "Médor", indentation=4)
>>> c.afficher()
--------------------------------------------------------------------------------
Objet de la classe Chien
Un chien qui est à la fois un animal et un carnivore
mro : [<class '__main__.Chien'>, <class '__main__.Affichable'>,
<class '__main__.Animal'>, <class '__main__.EtreVivant'>, <class 'object'>]
point_de_vie = 40
nom = Médor
--------------------------------------------------------------------------------
Il y a plusieurs points importants à remarquer dans l’implémentation ci-dessus :
La classe
Chien
hérite en premier du mixinAffichable
. À cause de ce que nous avons vu à propos du mro, un mixin devrait toujours être déclaré avant la classe parente.La classe
Chien
accepte comme dernier paramètre de constructeur le paramètre de compactage**kwargs
. La classeChien
peut ainsi accepter les paramètres nommésidentation
etseparateur
sans avoir besoin de les répéter.La classe
Affichage
a un constructeur assez compliqué.def __init__(self, *args, indentation=0, separateur="-", **kwargs): self.__indentation = indentation self.__separateur = separateur super().__init__(*args, **kwargs)
Les paramètres
indentation
etseparateur
sont placés après l’opérateur de compactage*args
ce qui implique qu’il s’agit de paramètres nommés. Le constructeur utilise à la fin la fonctionsuper()
pour appeler le constructeur suivant en lui passant tous les paramètres que le mixin ne reconnaît pas. On voit qu’il est tout à fait possible de concevoir un mixin qui accepte des paramètres de constructeur tout en s’intégrant élégamment à un héritage existant… au prix d’une petite complexité dans la déclaration du constructeur.
Note
Les mixins sont une façon de construire de nouvelles classes par agrégat de fonctionnalités. Attention cependant car en programmation orienté objet, il est recommandé de définir ses classes de manière à ce qu’elles aient une responsabilité unique et clairement identifiable dans le système. Cela permet de limiter la complexité des classes et donc de faciliter leur compréhension. L’abus du recours au mixin risque de créer des classes tout-en-un difficiles à comprendre et à maintenir.
La méta-classe¶
La méta-classe est un concept avancé en Python qui n’est que très très rarement utilisé directement par les développeurs.
En Python, les classes sont elles-mêmes des objets qui héritent de type
.
Il est possible de spécifier le type dont doit hériter l’objet qui représente
la classe. On parle alors de méta-classe.
Une méta-classe est une classe qui décrit une classe. Cela signifie que tous les attributs et toutes les méthodes d’une méta-classe seront les attributs et les méthodes de la classe.
L’usage de la méta-classe permet de réaliser des implémentations qui ne sont pas
possibles avec une simple classe. Par exemple, il n’est pas possible en Python
de déclarer une propriété de classe avec le décorateur @property
.
Mais cela devient possible par le truchement d’une méta-classe.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class MetaclasseCompteur(type):
"""Une méta-classe pour aider à compter les instances créées."""
def __init__(cls, *args, **kwargs):
super().__init__(*args, **kwargs)
cls._nb_instances = 0
@property
def nb_instances(cls):
return cls._nb_instances
def plus_une_instance(cls):
cls._nb_instances += 1
class MaClasse(metaclass=MetaclasseCompteur):
def __init__(self):
MaClasse.plus_une_instance()
|
Une méta-classe doit hériter directement ou indirectement de type
. À la
ligne 16, on déclare la méta-classe de la classe MaClasse
. Cette méta-classe
va fournir la propriété de classe nb_instances
ainsi que la méthode de classe
plus_une_instance()
.
>>> MaClasse.nb_instances
0
>>> m1 = MaClasse()
>>> m2 = MaClasse()
>>> m3 = MaClasse()
>>> MaClasse.nb_instances
3
Classes et méthodes abstraites¶
Comme Python est un langage à typage dynamique, nous avons déjà dit plus haut que la plupart des développeurs favorisent le duck typing : le type réel d’un objet importe moins que le fait qu’il produise les comportements attendus (c’est-à-dire les méthodes et les propriétés attendues). Cette approche amène indéniablement plus de souplesse dans la conception des applications. Mais elle rend plus difficile la validation du code et donc cela peut aboutir à la production d’un code moins robuste.
Les langages de programmation à typage fort comme C++, Java ou C# introduisent tous le principe de classes abstraites et/ou d’interfaces. Ce type d’approche insiste sur le fait de pouvoir définir un type particulier contenant un certain nombre de méthodes mais pour lesquelles on ne fournit aucune implémentation. Ces classes abstraites et ces interfaces sont ensuite héritées ou implémentées par d’autres classes qui doivent fournir les implémentations des méthodes attendues. Cela permet de garantir qu’un objet aura bien les comportements attendus, c’est-à-dire implémentera les méthodes attendues. On parle parfois de programmation par contrat, dans le sens où ces classes abstraites et ces interfaces sont comme des contrats qui lient les objets qui les implémentent et les objets qui appellent ces méthodes.
En Python, le module abc
permet de simuler ce type d’approche.
Le nom de ce module est la contraction de abstract base classes. Ce module
fournit une méta-classe appelée ABCMeta
qui permet
de transformer une classe Python en classe abstraite. Ce module fournit également
le décorateur @abstractmethod
qui permet de déclarer
comme abstraite une méthode, une méthode statique, une méthode de classe ou une
propriété. Cela signifie qu’il n’est pas possible de créer une instance
d’une classe qui hérite d’une classe abstraite tant que toutes les méthodes
abstraites ne sont pas implémentées.
Si nous reprenons notre exemple de la classe Animal
. Cette classe déclare
une méthode crier()
pour laquelle il n’est pas vraiment possible de fournir
une implémentation correcte pour un animal. On peut donc
considérer que la classe Animal
est abstraite en déclarant que sa
méta-classe est ABCMeta
et déclarer la méthode crier()
comme une méthode abstraite.
from abc import ABCMeta, abstractmethod
class Animal(metaclass=ABCMeta):
@abstractmethod
def crier(self):
pass
Toute tentative de créer un objet de type Animal
échouera à l’exécution :
>>> a = Animal()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Animal with abstract methods crier
La classe Animal
est en quelque sorte devenu un contrat qui indique que toute
classe qui en hérite doit fournir une implémentation pour la méthode crier()
.
class Chien(Animal):
def crier(self):
print("whouaf whouaf !")
>>> c = Chien()
>>> c.crier()
whouaf whouaf !