• Aucun résultat trouvé

qui est particulièrement complète et très bien faite.

19.4 Héritage

19.4.1 Prise en main

L’héritage peut évoquer la capacité qu’ont nos parents à nous transmettre certains traits physiques ou de caractère (ne dit-on pas, j’ai hérité ceci ou cela de ma mère ou de mon père ?). Qu’en est-il en programmation ?

Définition

En programmation, l’héritage est la capacité d’une classe d’hériter des propriétés d’une classe pré-existante. On parle de classe mère et de classe fille. En Python, l’héritage peut être multiple lorsqu’une classe fille hérite de plusieurs classes mères.

En Python, lorsque l’on veut créer une classe héritant d’une autre classe, on ajoutera après le nom de la classe fille le nom de la ou des classe(s) mère(s) entre parenthèses :

1 class Mere1 :

2 # contenu de la classe mère 1 3

4

5 class Mere2 :

6 # contenu de la classe mère 2 7

8

9 class Fille1 ( Mere1 ):

10 # contenu de la classe fille 1 11

12

13 class Fille2 (Mere1 , Mere2 ):

14 # contenu de la classe fille 2

Dans cet exemple, la classeFille1 hérite de la classe Mere1 et la classe Fille2 hérite des deux classes Mere1 et Mere2. Dans le cas de la classeFille2, on parle d’héritage multiple. Voyons maintenant un exemple concret :

1 class Mere :

2 def bonjour ( self ):

3 return " Vous avez le bonjour de la classe mère !" 4

5

6 class Fille ( Mere ): 7 def salut ( self ):

8 return "Un salut de la classe fille !" 9

10

11 if __name__ == " __main__ ": 12 fille = Fille ()

13 print ( fille . salut ()) 14 print ( fille . bonjour ())

Lignes 1 à 3. On définit une classeMere avec une méthode .bonjour().

Lignes 6 à 8. On définit une classeFille qui hérite de la classe Mere. Cette classe fille contient une nouvelle méthode .salut().

Lignes 12 à 14. Après instanciation de la classeFille, on utilise la méthode .salut(), puis la méthode .bonjour() héritée de la classe mère.

Ce code affiche la sortie suivante :

1 Un salut de la classe fille !

2 Vous avez le bonjour de la classe mère !

Nous commençons à entrevoir la puissance de l’héritage. Si on possède une classe avec de nombreuses méthodes et que l’on souhaite en ajouter de nouvelles, il suffit de créer une classe fille héritant d’une classe mère.

En revenant à notre exemple, une instance de la classe Fille sera automatiquement une instance de la classe Mere. Regardons dans l’interpréteur :

1 >>> fille = Fille ()

2 >>> isinstance (fille , Fille ) 3 True

4 >>> isinstance (fille , Mere ) 5 True

19.4. Héritage Chapitre 19. Avoir la classe avec les objets

Si une méthode de la classe fille possède le même nom que celle de la classe mère, c’est la première qui prend la prio-rité. Dans ce cas, on dit que la méthode est redéfinie (en anglais on parle de method overriding), tout comme on parlait de redéfinition des opérateursun peu plus haut. C’est le même mécanisme, car la redéfinition des opérateurs revient finalement à redéfinir une méthode magique (comme par exemple la méthode.__add__() pour l’opérateur +).

Voyons un exemple :

1 class Mere :

2 def bonjour ( self ):

3 return " Vous avez le bonjour de la classe mère !" 4

5

6 class Fille2 ( Mere ): 7 def bonjour ( self ):

8 return " Vous avez le bonjour de la classe fille !" 9

10

11 if __name__ == " __main__ ": 12 fille = Fille2 () 13 print ( fille . bonjour ())

Ce code va afficherVous avez le bonjour de la classe fille !. La méthode .bonjour() de la classe fille a donc pris la priorité sur celle de la classe mère. Ce comportement provient de la gestion des espaces de noms par Python, il est traité en détail dans la rubrique suivante.

Remarque

À ce point, nous pouvons faire une note de sémantique importante. Python utilise le mécanisme de redéfinition de méthode (method overriding), c’est-à-dire qu’on redéfinit une méthode héritée d’une classe mère. Il ne faut pas confondre cela avec la surcharge de fonction(function overloading) qui désigne le fait d’avoir plusieurs définitions d’une fonction selon le nombres d’arguments et/ou leur type (la surcharge n’est pas supportée par Python contrairement à d’autres langages orientés objet).

19.4.2 Ordre de résolution des noms

Vous l’avez compris, il y aura un ordre pour la résolution des noms d’attributs ou de méthodes en fonction du ou des héritage(s) de notre classe (à nouveau, cela provient de la manière dont Python gère les espaces de noms). Prenons l’exemple d’une classe déclarée comme suitclass Fille(Mere1, Mere2):. Si on invoque un attribut ou une méthode sur une instance de cette classe, Python cherchera d’abord dans la classeFille. S’il ne trouve pas, il cherchera ensuite dans la première classe mère (Mere1 dans notre exemple). S’il ne trouve pas, il cherchera dans les ancêtres de cette première mère (si elle en a), et ce en remontant la filiation (d’abord la grand-mère, puis l’arrière grand-mère, etc). S’il n’a toujours pas trouvé, il cherchera dans la deuxième classe mère (Mere2 dans notre exemple) puis dans tous ses ancêtres. Et ainsi de suite, s’il y a plus de deux classes mères. Bien sûr, si aucun attribut ou méthode n’est trouvé, Python renverra une erreur.

Il est en général possible d’avoir des informations sur l’ordre de résolution des méthodes d’une classe en évoquant la commandehelp() sur celle-ci ou une de ses instances. Par exemple, nous verrons dans le chapitre suivant le module Tkinter, imaginons que nous créions une instance de la classe principale du module Tkinter nomméeTk :

1 >>> import tkinter as tk 2 >>> racine = tk.Tk ()

En invoquant la commandehelp(racine), l’interpréteur nous montre :

1 Help on class Tk in module tkinter : 2

3 class Tk(Misc , Wm)

4 | Toplevel widget of Tk which represents mostly the main window 5 | of an application . It has an associated Tcl interpreter . 6 |

7 | Method resolution order : 8 | Tk

9 | Misc 10 | Wm

11 | builtins . object 12 [...]

On voit tout de suite que la classeTk hérite de deux autres classes Misc et Wm. Ensuite, le help indique l’ordre de résolution des méthodes : d’abord la classeTk elle-même, ensuite ses deux mères Misc puis Wm, et enfin une dernière classe nommée builtins.object dont nous allons voir la signification maintenant.

Chapitre 19. Avoir la classe avec les objets 19.4. Héritage

Remarque

En Python, il existe une classe interne nomméeobject qui est en quelque sorte la classe ancêtre de tous les objets. Toutes les classes héritent deobject.

Pour vous en convaincre, nous pouvons recréer une classe vide :

1 >>> class Citron : 2 ... pass

Puis ensuite regarder l’aide sur l’une de ses instances :

1 Help on class Citron in module __main__ : 2

3 class Citron ( builtins . object ) 4 | Data descriptors defined here : 5 |

6 | __dict__

7 | dictionary for instance variables (if defined ) 8 [...]

L’aide nous montre queCitron a hérité de builtins.object bien que nous ne l’ayons pas déclaré explicitement. Cela se fait donc de manière implicite.

Remarque

Le module builtins possède toutes les fonctions internes à Python. Il est donc pratique pour avoir une liste de toutes ces fonctions internes en un coup d’œil. Regardons cela avec les deux instructionsimport builtins puis dir(builtins) :

1 >>> import builtins 2 >>> dir ( builtins )

3 [' ArithmeticError ', 'AssertionError ', 'AttributeError ', [...]

4 'ascii ', 'bin ', 'bool ', 'bytearray ', 'bytes ', 'callable ', 'chr ', [...] 5 'list ', 'locals ', 'map ', 'max ', 'memoryview ', 'min ', 'next ', 'object ', [...] 6 'str ', 'sum ', 'super ', 'tuple ', 'type ', 'vars ', 'zip ']

Au début, on y trouve les exceptions commençant par une lettre majuscule (cf. chapitre 21 Remarques complémen-taires pour la définition d’une exception), puis les fonctions Python de base tout en minuscule. On retrouve par exemple list ou str, mais il y a aussi object. Toutefois ces fonctions étant chargées de base dans l’interpréteur, l’importation de builtins n’est pas obligatoire : par exemple list revient au même que builtins.list, ou object revient au même que builtins.object.

En résumé, la syntaxeclass Citron: sera équivalente à class Citron(builtins.object):

ou àclass Citron(object):.

Ainsi, même si on crée une classeCitron vide (contenant seulement une commande pass), elle possède déjà tout une panoplie de méthodes héritées de la classeobject. Regardez l’exemple suivant :

1 >>> class Citron : 2 ... pass 3 ...

4 >>> c = Citron () 5 >>> dir (c)

6 [' __class__ ', '__delattr__ ', '__dict__ ', '__dir__ ', '__doc__ ', '__eq__ ', 7 '__format__ ', '__ge__ ', '__getattribute__ ', '__gt__ ', '__hash__ ',

8 '__init__ ', '__le__ ', '__lt__ ', '__module__ ', '__ne__ ', '__new__ ', 9 '__reduce__ ', '__reduce_ex__ ', '__repr__ ', '__setattr__ ', '__sizeof__ ', 10 '__str__ ', '__subclasshook__ ', '__weakref__ ']

11 >>> o = object () 12 >>> dir (o)

13 [' __class__ ', '__delattr__ ', '__dir__ ', '__doc__ ', '__eq__ ', '__format__ ', 14 '__ge__ ', '__getattribute__ ', '__gt__ ', '__hash__ ', '__init__ ', '__le__ ', 15 '__lt__ ', '__ne__ ', '__new__ ', '__reduce__ ', '__reduce_ex__ ', '__repr__ ', 16 '__setattr__ ', '__sizeof__ ', '__str__ ', '__subclasshook__ ']

La quasi-totalité des attributs / méthodes de base de la classeCitron sont donc hérités de la classe object. Par exemple, lorsqu’on instancie un objet Citronc = Citron(), Python utilisera la méthode .__init__() héritée de la classe object (puisque nous ne l’avons pas définie dans la classeCitron).

19.4. Héritage Chapitre 19. Avoir la classe avec les objets

19.4.3 Un exemple concret d’héritage

Nous allons maintenant prendre un exemple un peu plus conséquent pour illustrer la puissance de l’héritage en programma-tion. D’abord quelques mots à propos de la concepprogramma-tion. Imaginons que nous souhaitions créer plusieurs classes correspondant à nos fruits favoris, par exemple le citron (comme par hasard !), l’orange, le kaki, etc. Chaque fruit a ses propres particularités, mais il y a aussi de nombreux points communs. Nous pourrions donc concevoir une classeFruit permettant, par exemple, d’instancier un fruit et ajouter des méthodes d’affichage commune à n’importe quel fruit, et ajouter (ou toute autre méthode) pouvant être utilisée pour n’importe quel fruit. Nous pourrions alors créer des classes commeCitron, Orange, etc., héritant de la classeFruit et ainsi nous économiser des lignes de code identiques à ajouter pour chaque fruit. Regardons l’exemple suivant que nous avons garni deprint() pour bien comprendre ce qui se passe :

1 class Fruit :

2 def __init__ (self , taille =None , masse =None , saveur =None , forme = None ): 3 print ("(2) Je suis dans le constructeur de la classe Fruit ") 4 self . taille = taille

5 self . masse = masse 6 self . saveur = saveur 7 self . forme = forme

8 print (" Je viens de créer self . taille , self .masse , self . saveur " 9 "et self . forme ")

10

11 def affiche_conseil (self , type_fruit , conseil ):

12 print ("(2) Je suis dans la mé thode . affiche_conseil () de la " 13 " classe Fruit \n")

14 return (" Instance {}\ ntaille : {}, masse : {} ,\n" 15 " saveur : {}, forme : {}\ nconseil : {}\ n"

16 . format ( type_fruit , self . taille , self .masse , self . saveur ,

17 self .forme , conseil ))

18 19

20 class Citron ( Fruit ):

21 def __init__ (self , taille =None , masse =None , saveur =None , forme = None ): 22 print ("(1) Je rentre dans le constructeur de Citron , et je vais " 23 " appeler \n"

24 "le constructeur de la classe mère Fruit !") 25 Fruit . __init__ (self , taille , masse , saveur , forme ) 26 print ("(3) J'ai fini dans le constructeur de Citron , "

27 " les attributs sont : \ nself . taille : {}, self . masse : {} ,\n" 28 " self . saveur : {}, self . forme : {}\ n"

29 . format ( self . taille , self .masse , self . saveur , self . forme )) 30

31 def __str__ ( self ):

32 print ("(1) Je rentre dans la mé thode . __str__ () de la classe " \ 33 " Citron ")

34 print (" Je vais lancer la mé thode . affiche_conseil () hé rit ée " \ 35 "de la classe Fruit ")

36 return self . affiche_conseil (" Citron ", " Bon en tarte :-p !") 37

38

39 if __name__ == " __main__ ": 40 # on crée un citron

41 citron1 = Citron ( taille =" petite ", saveur =" acide ", forme =" ellipso ïde",

42 masse =50)

43 print ( citron1 )

Lignes 1 à 9. On crée la classeFruit avec son constructeur qui initialisera tous les attributs d’instance décrivant le fruit. Lignes 11 à 17. Création d’une méthode.affiche_conseil() qui retourne une chaîne contenant le type de fruit, les attributs d’instance du fruit, et un conseil de consommation.

Lignes 20 à 29. Création de la classeCitron qui hérite de la classe Fruit. Le constructeur de Citron prend les mêmes arguments que ceux du constructeur deFruit. La ligne 24 est une étape importante que nous n’avons encore jamais vue : l’ins-tructionFruit.__init__() est un appel au constructeur de la classe mère (cf. explications plus bas). Notez bien que le pre-mier argument passé au constructeur de la classe mère sera systématiquement l’instance en coursself. Le print() en lignes 26-29 illustre qu’après l’appel du constructeur de la classe mère tous les attributs d’instance (self.taille, self.poids, etc.) ont bel et bien été créés.

Lignes 31 à 36. On définit la méthode.__str__() qui va modifier le comportement de notre classe avec print(). Celle-ci fait également appel à une méthode hértitée de la classe mère nommée.affiche_conseil(). Comme on a l’a héritée, elle est directement accessible avec unself.méthode() (et de l’extérieur ce serait instance.méthode()).

Lignes 39 à 43. Dans le programme principal, on instancie un objetCitron, puis on utilise print() sur l’instance. L’exécution de ce code affichera la sortie suivante :

1 (1) Je rentre dans le constructeur de Citron , et je vais appeler

Chapitre 19. Avoir la classe avec les objets 19.4. Héritage

2 le constructeur de la classe mère Fruit !

3 (2) Je suis dans le constructeur de la classe Fruit

4 Je viens de créer self . taille , self .masse , self . saveur et self . forme 5 (3) J'ai fini dans le constructeur de Citron , les attributs sont : 6 self . taille : petite , self . masse : 50,

7 self . saveur : acide , self . forme : ellipso ïde 8

9 (1) Je rentre dans la mé thode . __str__ () de la classe Citron

10 Je vais lancer la mé thode . affiche_conseil () hé rit ée de la classe Fruit 11 (2) Je suis dans la mé thode . affiche_conseil () de la classe Fruit 12

13 Instance Citron

14 taille : petite , masse : 50, 15 saveur : acide , forme : ellipso ïde 16 conseil : Bon en tarte :-p !

Prenez bien le temps de suivre ce code pas à pas pour bien en comprendre toutes les étapes.

Vous pourrez vous poser la question « Pourquoi utilise-t-on en ligne 24 la syntaxeFruit.__init__()? ». Cette syntaxe est souvent utilisée lorsqu’une classe hérite d’une autre classe pour faire appel au constructeur de la classe mère. La raison est que nous souhaitons appeler une méthode de la classe mère qui a le même nom qu’une méthode de la classe fille. Dans ce cas, si on utilisaitself.__init__(), cela correspondrait à la fonction de notre classe fille Citron. En mettant systématiquement une syntaxe

ClasseMere.__init__() on indique sans ambiguïté qu’on appelle le constructeur de la classe mère, en mettant expli-citement son nom. Ce mécanisme est assez souvent utilisé dans le module Tkinter (voir chapitre 20) pour la construction d’interfaces graphiques, nous en verrons de nombreux exemples.

Remarque

Si vous utilisez des ressources externes, il se peut que vous rencontriez une syntaxesuper().__init__(). La fonction Python internesuper() appelle automatiquement la classe mère sans que vous ayez à donner son nom. Même si cela peut paraître pratique, nous vous conseillons d’utiliser dans un premier temps la syntaxe

ClasseMere.__init__() qui est selon nous plus lisible (on voit explicitement le nom de la classe employée, même s’il y a plusieurs classes mères).

Ce mécanisme n’est pas obligatoirement utilisé, mais il est très utile lorsqu’une classe fille a besoin d’initialiser des attributs définis dans la classe mère. On le croise donc souvent car :

— Cela donne la garantie que toutes les variables de la classe mère sont bien initialisées. On réduit ainsi les risques de dysfonctionnement des méthodes héritées de la classe mère.

— Finalement, autant ré-utiliser les « moulinettes » de la classe mère, c’est justement à ça que sert l’héritage ! Au final, on écrit moins de lignes de code.

Conseil

Pour les deux raisons citées ci-dessus, nous vous conseillons de systématiquement utiliser le constructeur de la classe mère lors de l’instanciation.

Vous avez à présent bien compris le fonctionnement du mécanisme de l’héritage. Dans notre exemple, nous pourrions créer de nouveaux fruits avec un minimum d’effort. Ceux-ci pourraient hériter de la classe mèreFruit à nouveau, et nous n’aurions pas à réécrire les mêmes méthodes pour chaque fruit, simplement à les appeler. Par exemple :

1 class Kaki ( Fruit ):

2 def __init__ (self , taille =None , masse =None , saveur =None , forme = None ): 3 Fruit . __init__ (self , taille , masse , saveur , forme )

4

5 def __str__ ( self ):

6 return Fruit . affiche_conseil (self , " Kaki ",

7 " Bon à manger cru , miam !")

8 9

10 class Orange ( Fruit ):

11 def __init__ (self , taille =None , masse =None , saveur =None , forme = None ): 12 Fruit . __init__ (self , taille , masse , saveur , forme )

13

14 def __str__ ( self ):