Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Licence CC BY-NC-ND Thierry Parmentelat & Arnaud Legout Inria - UCA

Complément - niveau basique

La notion d’héritage, qui fait partie intégrante de la Programmation Orientée Objet, permet principalement de maximiser la réutilisabilité.

Nous avons vu dans la vidéo les mécanismes d’héritage in abstracto. Pour résumer très brièvement, on recherche un attribut (pour notre propos, disons une méthode) à partir d’une instance en cherchant :

L’objet de ce complément est de vous donner, d’un point de vue plus appliqué, des idées de ce que l’on peut faire ou non avec ce mécanisme. Le sujet étant assez rabâché par ailleurs, nous nous concentrerons sur deux points :

Plusieurs formes d’héritage

Une méthode héritée peut être rangée dans une de ces trois catégories :

Commençons par illustrer tout ceci sur un petit exemple :

# Une classe mère
class Fleur:
    def implicite(self):
        print('Fleur.implicite')
    def redefinie(self):
        print('Fleur.redéfinie')
    def modifiee(self):
        print('Fleur.modifiée')

# Une classe fille
class Rose(Fleur):
    # on ne définit pas implicite
    # on rédéfinit complètement redefinie
    def redefinie(self):
        print('Rose.redefinie')
    # on change un peu le comportement de modifiee
    def modifiee(self):
        Fleur.modifiee(self)
        print('Rose.modifiee apres Fleur')

On peut à présent créer une instance de Rose et appeler sur cette instance les trois méthodes.

# fille est une instance de Rose
fille = Rose()

fille.implicite()
Fleur.implicite
fille.redefinie()
Rose.redefinie

S’agissant des deux premières méthodes, le comportement qu’on observe est simplement la conséquence de l’algorithme de recherche d’attributs : implicite est trouvée dans la classe Fleur et redefinie est trouvée dans la classe Rose.

fille.modifiee()
Fleur.modifiée
Rose.modifiee apres Fleur

Pour la troisième méthode, attardons-nous un peu car on voit ici comment combiner facilement le code de la classe mère avec du code spécifique à la classe fille, en appelant explicitement la méthode de la classe mère lorsqu’on fait :

Fleur.modifiee(self)

La fonction built-in super()

Signalons à ce sujet, pour être exhaustif, l’existence de la fonction built-in super() qui permet de réaliser la même chose sans nommer explicitement la classe mère, comme on le fait ici :

# Une version allégée de la classe fille, qui utilise super()
class Rose(Fleur):
    def modifiee(self):
        super().modifiee()
        print('Rose.modifiee apres Fleur')
fille = Rose()

fille.modifiee()
Fleur.modifiée
Rose.modifiee apres Fleur

On peut envisager d’utiliser super() dans du code très abstrait où on ne sait pas déterminer statiquement le nom de la classe dont il est question. Mais, c’est une question de goût évidemment, je recommande personnellement la première forme, où on qualifie la méthode avec le nom de la classe mère qu’on souhaite utiliser. En effet, assez souvent :

Héritage vs Composition

Dans le domaine de la conception orientée objet, on fait la différence entre deux constructions, l’héritage et la composition, qui à une analyse superficielle peuvent paraître procurer des résultats similaires, mais qu’il est important de bien distinguer.

Voyons d’abord en quoi consiste la composition et pourquoi le résultat est voisin :

# Une classe avec qui on n'aura pas de relation d'héritage
class Tige:
    def implicite(self):
        print('Tige.implicite')
    def redefinie(self):
        print('Tige.redefinie')
    def modifiee(self):
        print('Tige.modifiee')

# on n'hérite pas
# mais on fait ce qu'on appelle une composition
# avec la classe Tige
class Rose:
    # mais pour chaque objet de la classe Rose
    # on va créer un objet de la classe Tige
    # et le conserver dans un champ
    def __init__(self):
        self.externe = Tige()

    # le reste est presque comme tout à l'heure
    # sauf qu'il faut definir implicite
    def implicite(self):
        self.externe.implicite()
        
    # on redéfinit complètement redefinie
    def redefinie(self):
        print('Rose.redefinie')
        
    def modifiee(self):
        self.externe.modifiee()
        print('Rose.modifiee apres Tige')
# on obtient ici exactement le même comportement 
# pour les trois sortes de méthodes
fille = Rose()

fille.implicite()
fille.redefinie()
fille.modifiee()
Tige.implicite
Rose.redefinie
Tige.modifiee
Rose.modifiee apres Tige

Comment choisir ?

Alors, quand faut-il utiliser l’héritage et quand faut-il utiliser la composition ?
On arrive ici à la limite de notre cours, il s’agit plus de conception que de codage à proprement parler, mais pour donner une réponse très courte à cette question :

Complément - niveau intermédiaire

Des exemples de code

Sans transition, dans cette section un peu plus prospective, nous vous avons signalé quelques morceaux de code de la bibliothèque standard qui utilisent l’héritage. Sans aller nécessairement jusqu’à la lecture de ces codes, il nous a semblé intéressant de commenter spécifiquement l’usage qui est fait de l’héritage dans ces bibliothèques.

Le module calendar

On trouve dans la bibliothèque standard le module calendar. Ce module expose deux classes TextCalendar et HTMLCalendar. Sans entrer du tout dans le détail, ces deux classes permettent d’imprimer dans des formats différents le même type d’informations.

Le point ici est que les concepteurs ont choisi un graphe d’héritage comme ceci :

    Calendar
    |-- TextCalendar
    |-- HTMLCalendar

qui permet de grouper le code concernant la logique dans la classe Calendar, puis dans les deux sous-classes d’implémenter le type de sortie qui va bien.

C’est l’utilisateur qui choisit la classe qui lui convient le mieux, et de cette manière, le maximum de code est partagé entre les deux classes ; et de plus si vous avez besoin d’une sortie au format, disons PDF, vous pouvez envisager d’hériter de Calendar et de n’implémenter que la partie spécifique au format PDF.

C’est un peu le niveau élémentaire de l’héritage.

Le module SocketServer

Toujours dans la bibliothèque standard, le module SocketServer fait un usage beaucoup plus sophistiqué de l’héritage.

Le module propose une hiérarchie de classes comme ceci :

    +------------+
    | BaseServer |
    +------------+
          |
          v
    +-----------+        +------------------+
    | TCPServer |------->| UnixStreamServer |
    +-----------+        +------------------+
          |
          v
    +-----------+        +--------------------+
    | UDPServer |------->| UnixDatagramServer |
    +-----------+        +--------------------+

Ici encore notre propos n’est pas d’entrer dans les détails, mais d’observer le fait que les différents niveaux d’intelligence sont ajoutés les uns aux les autres au fur et à mesure que l’on descend le graphe d’héritage.

Cette hiérarchie est par ailleurs étendue par le module http.server et notamment au travers de la classe HTTPServer qui hérite de TCPServer.

Pour revenir au module SocketServer, j’attire votre attention dans la page d’exemples sur cet exemple en particuler, où on crée une classe de serveurs multi-threads - la classe ThreadedTCPServer - par simple héritage multiple entre ThreadingMixIn et TCPServer. La notion de Mixin est décrite dans cette page Wikipédia dans laquelle vous pouvez accéder directement à la section consacrée à Python.