L’héritage des classes Python et l’ordre de résolution des méthodes

L’héritage des classes Python a été introduit dans le premier tutoriel sur les classes. Nous avons vu les différents types d’héritage que nous pouvons utiliser afin de pouvoir éviter la répétition du code.

Dans plusieurs scénarios d’héritage, la recherche d’attributs ou de méthodes commence dans la classe actuelle. Si Python ne trouve rien, la recherche se poursuivra dans les classes parentes en profondeur et de gauche à droite.

Dans l’héritage des classes Python une telle recherche et dans cet ordre est appelée Ordre de résolution des méthodes ou en anglais « methode resolution order » (MRO).

Maintenant, considérons un scénario où la classe parente et enfant contiennent des méthodes avec le même nom. C’est ce qu’on appelle un remplacement de méthode (overriding).

Les trois principes de la MRO dans l’héritage des classes Python

1- Rechercher la sous-classe avant de rechercher ses classes de base. Si la classe B hérite de A, la recherche s’effectue d’abord dans B, puis passera à A.
2- Si une classe hérite de plusieurs classes, la priorité de la recherche suit la règle du premier chemin directe vers la première classe parente par rapport à la règle du chemin gauche-droite.
3- La recherche dans une classe ne se fait pas plus d’une fois. Cela signifie qu’une classe de la hiérarchie dans l’héritage des classes Python n’est parcourue qu’une seule fois.
En réalité, comprendre l’ordre de résolution des méthodes vous donne une idée claire sur les classes qui seront exécutées et dans quel ordre. Il existe une méthode prédéfinie qui renvoie l’ordre d’exécution des classes Python.
C’est : nom_de_la_classe.mro() – prenons quelques exemples :

class A:
   def m1(self):
       print("La méthode m1 de la classe A")

class B(A):
   def m1(self):
       print("La méthode m1 de la classe B")

class C(A):
   def m1(self):
       print("La méthode m1 de la classe C")

class D(B, C):
   def m1(self):
       print("La méthode m1 de la classe D")

print(A.mro())
print(B.mro())
print(C.mro())
print(D.mro())

L’exécution du code :

[<class '__main__.A'>, <class 'object'>]
[<class '__main__.B'>, <class '__main__.A'>, <class 'object'>]
[<class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

À présent nous allons créer une instance de la classe C.

c = C()
c.m1()
print(C.mro())

L’exécution du code :

La méthode m1 de la classe C
[<class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

Dans le programme ci-dessus, l’objet c appelle la méthode m1(). Puisque, m1() est disponible dans la classe C, donc la sortie sera à partir de la classe actuelle C.

Un autre exemple

Maintenant, modifions notre exemple pour que la méthode m1() ne soit pas présente dans la classe C.

class A:
   def m1(self):
       print("La méthode m1 de la classe A")

class B(A):
   def m1(self):
       print("La méthode m1 de la classe B")

class C(A):
   def m2(self):
       print("La méthode m2 de la classe C")

class D(B, C):
   def m1(self):
       print("La méthode m1 de la classe D")

c =C()
c.m1()
print(C.mro())

L’exécution du code :

La méthode m1 de la classe A
[<class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

En fait, la méthode m1() n’existe pas dans la classe C. Donc, la recherche se fera dans la classe C, puis dans la classe A, car A est la superclasse de C. Par conséquent, la sortie se fera à partir de la classe A puisqu’elle contient la méthode m1().

Donc, on applique la règle du premier chemin directe vers la première classe parente avec un niveau supplémentaire d’héritage. La classe C est une sous-classe de B et B est une sous-classe de A.

La règle du chemin gauche-droite

À présent, prenons un autre exemple en modifions la classe D.

class A:
   def m1(self):
       print("La méthode m1 de la classe A")

class B(A):
   def m1(self):
       print("La méthode m1 de la classe B")

class C(A):
   def m1(self):
       print("La méthode m1 de la classe C")

class D(B, C):
   def m3(self):
       print("La méthode m3 de la classe D")

d = D()
d.m1()
print(D.mro())

L’exécution du code :

La méthode m1 de la classe B
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

Comme la méthode m1() ne se trouve pas dans la classe D, la cherche se fera dans la classe B, car c’est la première superclasse de la classe D à partir de la gauche. Puisque la méthode m1()existe dans la classe B, c’est elle qui sera renvoyée.

Partant du même principe, si vous enlevez la méthode m1() de la classe B, elle sera recherché dans la classe C.

L’héritage des classes Python complexes et la mro

Jusqu’ici, nous avons vu des cas simples que vous pouvez deviner sans même utiliser la méthodes mro(). Cependant, ce n’est toujours pas le cas dans les héritages des classes Python les plus complexes.

Toujours avec l’exemple précédent, si m1() n’est pas dans la classe B, vous l’avez certainement deviné. La recherche se fera dans la classe C.

class A:
   def m1(self):
       print("La méthode m1 de la classe A")

class B(A):
   def m2(self):
       print("La méthode m2 de la classe B")

class C(A):
   def m1(self):
       print("La méthode m1 de la classe C")

class D(B, C):
   def m3(self):
       print("La méthode m3 de la classe D")

d = D()
d.m1()
print(D.mro())
La méthode m1 de la classe C
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

Toutefois, si vous gardez le même code mais avec une toute petite modification. Faisons en sorte que la classe C n’hérite pas de la classe la plus profonde A.

class A:
   def m1(self):
       print("La méthode m1 de la classe A")

class B(A):
   def m2(self):
       print("La méthode m2 de la classe B")

class C():
   def m1(self):
       print("La méthode m1 de la classe C")

class D(B, C):
   def m3(self):
       print("La méthode m3 de la classe D")

d = D()
d.m1()
print(D.mro())
La méthode m1 de la classe A
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.A'>, <class '__main__.C'>, <class 'object'>]

Surprise ! même si la classe D hérite de la classe C, la recherche a sauté vers la classe A.

Eh bien, c’est la règle du premier chemin directe vers la classe la plus profonde qui a la priorité sur la règle gauche-droite. La classe D hérite de la classe B qui elle-même hérite de la classe A. Mais, contrairement à l’exemple précédent, la classe C n’hérite pas de A, donc la recherche se fait au niveau de la classe A à travers la classe B.

Plus d’exemples

Encore un exemple, essayez de deviner le chemin de la recherche en regardant le code et en appliquant les deux règles précédentes avant d’exécuter le code.

class A:
   def m1(self):
       print("La méthode m1 de la classe A")

class B(A):
   def m2(self):
       print("La méthode m2 de la classe B")

class C(B):
   def m3(self):
       print("La méthode m3 de la classe C")

class D(B):
   def m4(self):
       print("La méthode m4 de la classe D")

class E(C,D):
    pass

e = E()
e.m1()
print(E.mro())

Si vous avez réussi, je vous dis bravo !

Sinon, regardez ce schéma qui pourrait vous aider à mieux comprendre.

l'héritage des classes Python-mro

A.mro() => A, object
B
.mro() => B, object
C
.mro() => C, object
X
.mro() => X, A, B, object
Y
.mro() => Y, B, C, object
P
.mro() => P, X, A, Y, B, C, object

Le code qui correspond à ce schéma :

class A:
   def m1(self):
       print("La méthode m1 de la classe A")
class B:
   def m1(self):
       print("La méthode m1 de la classe B")
class C:
   def m1(self):
       print("La méthode m1 de la classe C")
class X(A, B):
   def m1(self):
       print("La méthode m1 de la classe X")
class Y(B, C):
   def m1(self):
       print("La méthode m1 de la classe Y")
class P(X, Y, C):
   def m1(self):
       print("La méthode m1 de la classe P")

print(A.mro())      #AO
print(X.mro())      #XABO
print(Y.mro())      #YBCO
print(P.mro())      #PXAYBCO

L’exécution du code :

[<class '__main__.A'>, <class 'object'>]
[<class '__main__.X'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>]
[<class '__main__.Y'>, <class '__main__.B'>, <class '__main__.C'>, <class 'object'>]
[<class '__main__.P'>, <class '__main__.X'>, <class '__main__.A'>, <class '__main__.Y'>, <class '__main__.B'>, <class '__main__.C'>, <class 'object'>]

Rappelez-vous qu’en cas de conflit, la règle du premier chemin directe vers la classe parente est utilisée avant la règle du chemin gauche-droite.

Ce sont donc les deux règles que Python suit pour détecter la méthode de classe à appeler dans l’héritage des classes Python.

L’héritage des classes Python et l’ordre de résolution des méthodes

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.

Retour en haut