Le guide ultime et définitif sur la programmation orientée objet en Python à l’usage des débutants qui sont rassurés par les textes détaillés qui prennent le temps de tout expliquer. Partie 4.
dimanche 3 février 2013 à 12:04Un peu de zik, puisque c’est ce qu’on fait maintenant :
Prérequis :
- Avoir lu (et compris) la partie 3
Aujourd’hui nous allons voir ce qui fait toute la puissance de la programmation orientée objet : l’héritage.
Héritage simple
L’héritage est un moyen de factoriser du code, c’est à dire que si on a le même code à deux endroits, l’héritage permet de centraliser ce code à un seul endroit. Ce n’est pas le seul moyen de faire ça. On peut très bien factoriser du code uniquement avec des fonctions. Néanmoins l’héritage a des caractéristiques qui rendent la factorisation très efficace.
Supposons que vous fassiez un jeu vidéo pour apprendre les prières chrétiennes, et que vous ayez une classe par prière :
class AveMaria: texte = ("Je vous salue, Marie pleine de grâce ;", "Le Seigneur est avec vous.", "Vous êtes bénie entre toutes les femmes", "Et Jésus, le fruit de vos entrailles, est béni.", "Sainte Marie, Mère de Dieu,", "Priez pour nous, pauvres pécheurs,", "Maintenant, et à l'heure de notre mort.", "Amen.") def prier(self, nombre_de_fois=1): for x in xrange(nombre_de_fois): for ligne in self.texte: print ligne class PaterNoster: texte = ("Notre Père qui es aux cieux", "que ton nom soit sanctifié", "que ton règne vienne,", "que ta volonté soit faite", "sur la terre comme au ciel.", "Donne-nous aujourd’hui", "notre pain de ce jour,", "pardonne-nous nos offenses", "comme nous pardonnons aussi", "à ceux qui nous ont offensés", "et ne nous soumets pas à la tentation", "mais délivre-nous du mal.", "Amen") def prier(self, nombre_de_fois=1): for x in xrange(nombre_de_fois): for ligne in self.texte: print ligne >>> piere = AveMaria() >>> piere.prier() Je vous salue, Marie pleine de grâce ; Le Seigneur est avec vous. Vous êtes bénie entre toutes les femmes Et Jésus, le fruit de vos entrailles, est béni. Sainte Marie, Mère de Dieu, Priez pour nous, pauvres pécheurs, Maintenant, et à l'heure de notre mort. Amen.
Ici la méthode prier()
est dupliquée. Or, c’est exactement la même. Il n’y a pas de raison de l’écrire deux fois. Nous allons utiliser l’héritage pour centraliser ce code en créant une classe Priere
qui contiendra le code commun à toutes les prières :
class Priere: def prier(self, nombre_de_fois=1): for x in xrange(nombre_de_fois): for ligne in self.texte: print ligne
Ensuite, nous allons demander à toutes les classes prières d’hériter de cette classe commune :
class AveMaria(Priere): # <---- cette syntaxe veut dire 'hérite de Priere' texte = ("Je vous salue, Marie pleine de grâce ;", "Le Seigneur est avec vous.", "Vous êtes bénie entre toutes les femmes", "Et Jésus, le fruit de vos entrailles, est béni.", "Sainte Marie, Mère de Dieu,", "Priez pour nous, pauvres pécheurs,", "Maintenant, et à l'heure de notre mort.", "Amen.") class PaterNoster(Priere): texte = ("Notre Père qui es aux cieux", "que ton nom soit sanctifié", "que ton règne vienne,", "que ta volonté soit faite", "sur la terre comme au ciel.", "Donne-nous aujourd’hui", "notre pain de ce jour,", "pardonne-nous nos offenses", "comme nous pardonnons aussi", "à ceux qui nous ont offensés", "et ne nous soumets pas à la tentation", "mais délivre-nous du mal.", "Amen")
Ici, les classes AveMaria
et PaterNoster
héritent de la classe Priere
. On dit qu’elles sont les enfants (ou filles) de Priere
. Ou que Priere
est la classe parente de AveMaria
et PaterNoster
Notez qu’on a retiré la méthode prier()
de PaterNoster
et AveMaria
. Malgré cela, ça marche :
>>> PaterNoster().prier() Notre Père qui es aux cieux que ton nom soit sanctifié que ton règne vienne, que ta volonté soit faite sur la terre comme au ciel. Donne-nous aujourd'hui notre pain de ce jour, pardonne-nous nos offenses comme nous pardonnons aussi à ceux qui nous ont offensés et ne nous soumets pas à la tentation mais délivre-nous du mal. Amen
Cela marche car quand on hérite d’une classe, le code de cette classe est copié de la classe parente vers la classe enfant. Ainsi, prier()
est automatiquement copiée de Priere
vers AveMaria
et PaterNostre
.
Cela marche pour toutes les méthodes, même celles appelées automatiquement. C’est particulièrement utile avec la méthode __init__
:
class Priere: def __init__(self, expiation=False): self.expiation = expiation def prier(self, nombre_de_fois=1): for x in xrange(nombre_de_fois): for ligne in self.texte: if self.expiation: print ligne.upper() else: print ligne
On rajoute ici un attribut, et il se retrouve dans la classe enfant (si vous êtes dans un shell, n’oubliez pas de réécrire aussi la classe AveMaria
à chaque fois, même si elle ne change pas):
>>> priere = AveMaria(expiation=True) >>> priere.expiation True >>> priere.prier() JE VOUS SALUE, MARIE PLEINE DE GRâCE ; LE SEIGNEUR EST AVEC VOUS. VOUS êTES BéNIE ENTRE TOUTES LES FEMMES ET JéSUS, LE FRUIT DE VOS ENTRAILLES, EST BéNI. SAINTE MARIE, MèRE DE DIEU, PRIEZ POUR NOUS, PAUVRES PéCHEURS, MAINTENANT, ET à L'HEURE DE NOTRE MORT. AMEN.
L’héritage, c’est juste cela. Pas la peine de chercher une truc compliqué : le code du parent se retrouve dans celui de l’enfant. Cela marche pour les méthodes et les attributs.
Petite appartée sur object
Dans la partie précédente et dans de nombreux codes, vous avez dû voir des classes comme cela :
class MaClass(object): # wtf is this object stuff ? # code
Il s’agit bel et bien d’héritage, object
étant un type buil-in en Python au même titre que les string ou les int :
>>> object <type 'object'>
La raison de cet héritage bizarre est qu’à partir de Python 2.2, une nouvelle architecture des classes a été introduite qui corrige les problèmes de l’ancienne. Mais pour garder le code compatible, ces nouvelles classes n’ont pas été activées par défaut.
Ainsi, même si vous êtes en Python 2.7, quand vous faites :
class MaClass: # code
Vous utilisez l’ancien type de classe (old-style class). Pour dire à Python que vous voulez utiliser le nouveau type de classe (new-style class), il faut hériter d’object
:
class MaClass(object): # code
Il n’y a AUCUN intérêt à utiliser les old-style classes. Je l’ai fais en ce début de cours pour éviter d’introduire la notion d’héritage trop tôt.
A partir de maintenant, quand vous créez une nouvelle classe qui n’a pas de parent, faites la TOUJOURS hériter de object
.
Des tas de fonctions (par exemple les properties) fonctionnent beaucoup mieux avec les new-style classes. (D’ailleurs à partir de Python 3, elles sont activées par défaut)
Ainsi, notre classe Priere doit ressembler à ça maintenant :
class Priere(object): def __init__(self, expiation=False): self.expiation = expiation def prier(self, nombre_de_fois=1): for x in xrange(nombre_de_fois): for ligne in self.texte: if self.expiation: print ligne.upper() else: print ligne
Overriding
Parfois on veut tout le code du parent dans l’enfant. Parfois juste une partie. L’héritage vous permet de réécrire certaines méthodes du parent dans l’enfant, c’est ce qu’on appelle l’overriding.
Quand vous faites :
class Parent(object): def truc(self): print 'foo' class Enfant(Parent): pass
En fait vous faites en quelque sorte :
class Enfant(object): def truc(self): print 'foo'
Si vous faites :
class Enfant(object): def truc(self): print 'foo' def truc(self): print 'bar'
Vous allez écraser la première méthode avec la deuxième, car elles portent toutes les deux le même nom :
>>> Enfant().truc() bar
“Ecraser” en anglais, se dit “to override”. Félicitation, vous venez d’apprendre l’overriding ! Ce qu’on vient de voir plus haut s’écrit comme ça dans le cadre de l’héritage :
class Parent(object): def truc(self): print 'foo' class Enfant1(Parent): pass # pas d'overriding class Enfant2(Parent): def truc(self): print 'bar' # overriding ! >>> Enfant1().truc() foo >>> Enfant2().truc() bar
Enfant1
a tout le code du parent qui est copié. Enfant2
aussi, mais il réécrit la méthode, donc sa version de la méthode écrase celle du parent.
Voyons ce que ça donne sur un cas plus concret dans la vie de tous les jours comme les prières chrétiennes (promis je fais les sourates du Coran si je fais un tuto Haskell) :
class Priere(object): def __init__(self, expiation=False): self.expiation = expiation def prier(self, nombre_de_fois=1): for x in xrange(nombre_de_fois): for ligne in self.texte: if self.expiation: print ligne.upper() else: print ligne class PaterNoster(Priere): texte = ("Notre Père qui es aux cieux", "que ton nom soit sanctifié", "que ton règne vienne,", "que ta volonté soit faite", "sur la terre comme au ciel.", "Donne-nous aujourd’hui", "notre pain de ce jour,", "pardonne-nous nos offenses", "comme nous pardonnons aussi", "à ceux qui nous ont offensés", "et ne nous soumets pas à la tentation", "mais délivre-nous du mal.", "Amen") # sur avemaria, on veut mettre en avant la version en araméen # par l'araméen c'est trop cool (ça ressemble au langage des furlings # dans stargate) class AveMaria(Priere): vf = ("Je vous salue, Marie pleine de grâce ;", "Le Seigneur est avec vous.", "Vous êtes bénie entre toutes les femmes", "Et Jésus, le fruit de vos entrailles, est béni.", "Sainte Marie, Mère de Dieu,", "Priez pour nous, pauvres pécheurs,", "Maintenant, et à l'heure de notre mort.", "Amen.") vo = ("ܡܠܝܬ ܛܝܒܘܬܐ", "ܡܪܢ ܥܡܟܝ", "ܡܒܪܟܬܐ ܐܢܬܝ ܒܢܫ̈ܐ", "ܘܡܒܪܟ ܗܘ ܦܐܪܐ ܕܒܟܪܣܟܝ ܡܪܢ ܝܫܘܥ", "ܐܘ ܩܕܝܫܬܐ ܡܪܝܡ ܝܠܕܬ ܐܠܗܐ", "ܨܠܝ ܚܠܦܝܢ ܚܛܝ̈ܐ", "ܗܫܐ ܘܒܫܥܬ ܘܡܘܬܢ", "ܐܡܝܢ܀") def prier(self, nombre_de_fois=1, version='vo'): for x in xrange(nombre_de_fois): for ligne in getattr(self, version, 'vo'): if self.expiation: print ligne.upper() else: print ligne >>> AveMaria().prier() ܡܠܝܬ ܛܝܒܘܬܐ ܡܪܢ ܥܡܟܝ ܡܒܪܟܬܐ ܐܢܬܝ ܒܢܫ̈ܐ ܘܡܒܪܟ ܗܘ ܦܐܪܐ ܕܒܟܪܣܟܝ ܡܪܢ ܝܫܘܥ ܐܘ ܩܕܝܫܬܐ ܡܪܝܡ ܝܠܕܬ ܐܠܗܐ ܨܠܝ ܚܠܦܝܢ ܚܛܝ̈ܐ ܗܫܐ ܘܒܫܥܬ ܘܡܘܬܢ ܐܡܝܢ܀ >>> AveMaria().prier(2, 'vf') Je vous salue, Marie pleine de grâce ; Le Seigneur est avec vous. Vous êtes bénie entre toutes les femmes Et Jésus, le fruit de vos entrailles, est béni. Sainte Marie, Mère de Dieu, Priez pour nous, pauvres pécheurs, Maintenant, et à l'heure de notre mort. Amen. Je vous salue, Marie pleine de grâce ; Le Seigneur est avec vous. Vous êtes bénie entre toutes les femmes Et Jésus, le fruit de vos entrailles, est béni. Sainte Marie, Mère de Dieu, Priez pour nous, pauvres pécheurs, Maintenant, et à l'heure de notre mort. Amen.
Ici la classe AveMaria
hérite de la classe Priere
. Deux méthodes sont copiées de Priere
vers AveMaria
: __init__
et prier()
.
__init__
ne change pas. Donc AveMaria
a toujours le __init__
de Priere
. Par contre, on a overridé prier()
dans AveMaria
, qui est maintenant un code personnalisé.
Ceci nous permet donc de bénéficier d’une partie du code en commun (__init__
), et de choisir un comportement différent pour d’autres bout du code (prier()
).
La classe PaterNoster
, elle, n’est pas affectée. Elle n’override rien, et sa méthode prier()
est la même que celle de Priere
.
Peut-être voulez-vous une exemple plus terre à terre (et moins dans les cieux) :
Prenez la bibliothèque path.py, par exemple. Normalement quand on additionne deux strings, ça les concatène. Mais quand on les divise, le comportement par défaut est de lever une erreur.
>>> '/home/sam' + '/blog' '/home/sam/blog' >>> '/home/sam' / '/blog' Traceback (most recent call last): File "<ipython-input-58-8701a4e82b4b>", line 1, in <module> '/home/sam' / '/blog' TypeError: unsupported operand type(s) for /: 'str' and 'str'
Path.py override ce comportement :
import os class path(str): # hé oui, on peut hériter des types de base def __div__(self, other): # override le comportement face à l'opérateur '/' return path(os.path.join(self, other)) ... p = path('/home/sam') ... print p / 'blog' /home/sam/blog
Ca nous donne une très jolie interface pour la manipulation des chemins d’accès.
Polymorphisme et autres diableries
Comme d’hab en prog, on adore les mots qui font super hype de la vibes du flex.
Le polymorphisme fait partie de ces termes compliqués qui cachent une notion simple : avoir une API commune qui fait des choses différentes.
Voyez-vous, nos deux classes AveMaria
et PaterNoster
, sont toutes les deux filles de la même classe parente. Elles se ressemblent donc beaucoup : elles ont des attributs et des méthodes en commun :
>>> p1 = AveMaria() >>> p2 = PaterNoster() >>> p1.expiation False >>> p2.expiation False >>> p1.prier <bound method AveMaria.prier of <__main__.AveMaria object at 0x20618d0>> >>> p2.prier <bound method PaterNoster.prier of <__main__.PaterNoster object at 0x2061c90>> >>> p1.__init__ <bound method AveMaria.__init__ of <__main__.AveMaria object at 0x20618d0>> >>> p2.__init__ <bound method PaterNoster.__init__ of <__main__.PaterNoster object at 0x2061c90>>
Ce sont pourtant des classes différentes (et prier()
ne fait pas du tout le même chose), mais elles partagent ce qu’on appelle une API (une interface), c’est à dire une manière de les utiliser.
Cette capacité à être utilisé pareil, mais produire un résultat diffférent est ce qu’on appelle le polymorphisme (et c’est la base du duck typing). Concrètement ça veut dire que vous pouvez utiliser les deux classes dans le même contexte, sans vous soucier de si c’est l’une ou l’autre :
>>> prieres = [AveMaria(), AveMaria(), PaterNoster(), AveMaria(), PaterNoster()] >>> prieres [<__main__.AveMaria object at 0x2061d50>, <__main__.AveMaria object at 0x2061d90>, <__main__.PaterNoster object at 0x2061f50>, <__main__.AveMaria object at 0x2061f90>, <__main__.PaterNoster object at 0x2061fd0>] >>> for priere in prieres: ... priere.prier() # prier une prière, ça se fait pareil ܡܠܝܬ ܛܝܒܘܬܐ ܡܪܢ ܥܡܟܝ ܡܒܪܟܬܐ ܐܢܬܝ ܒܢܫ̈ܐ ܘܡܒܪܟ ܗܘ ܦܐܪܐ ܕܒܟܪܣܟܝ ܡܪܢ ܝܫܘܥ ܐܘ ܩܕܝܫܬܐ ܡܪܝܡ ܝܠܕܬ ܐܠܗܐ ܨܠܝ ܚܠܦܝܢ ܚܛܝ̈ܐ ܗܫܐ ܘܒܫܥܬ ܘܡܘܬܢ ܐܡܝܢ܀ ܡܠܝܬ ܛܝܒܘܬܐ ܡܪܢ ܥܡܟܝ ܡܒܪܟܬܐ ܐܢܬܝ ܒܢܫ̈ܐ ܘܡܒܪܟ ܗܘ ܦܐܪܐ ܕܒܟܪܣܟܝ ܡܪܢ ܝܫܘܥ ܐܘ ܩܕܝܫܬܐ ܡܪܝܡ ܝܠܕܬ ܐܠܗܐ ܨܠܝ ܚܠܦܝܢ ܚܛܝ̈ܐ ܗܫܐ ܘܒܫܥܬ ܘܡܘܬܢ ܐܡܝܢ܀ Notre Père qui es aux cieux que ton nom soit sanctifié que ton règne vienne, que ta volonté soit faite sur la terre comme au ciel. Donne-nous aujourd’hui notre pain de ce jour, pardonne-nous nos offenses comme nous pardonnons aussi à ceux qui nous ont offensés et ne nous soumets pas à la tentation mais délivre-nous du mal. Amen ܡܠܝܬ ܛܝܒܘܬܐ ܡܪܢ ܥܡܟܝ ܡܒܪܟܬܐ ܐܢܬܝ ܒܢܫ̈ܐ ܘܡܒܪܟ ܗܘ ܦܐܪܐ ܕܒܟܪܣܟܝ ܡܪܢ ܝܫܘܥ ܐܘ ܩܕܝܫܬܐ ܡܪܝܡ ܝܠܕܬ ܐܠܗܐ ܨܠܝ ܚܠܦܝܢ ܚܛܝ̈ܐ ��ܫܐ ܘܒܫܥܬ ܘܡܘܬܢ ܐܡܝܢ܀ Notre Père qui es aux cieux que ton nom soit sanctifié que ton règne vienne, que ta volonté soit faite sur la terre comme au ciel. Donne-nous aujourd’hui notre pain de ce jour, pardonne-nous nos offenses comme nous pardonnons aussi à ceux qui nous ont offensés et ne nous soumets pas à la tentation mais délivre-nous du mal. Amen
Le polymorphisme, c’est donc l’utilisation de l’héritage pour faire des choses différentes, mais en proposant la même interface (ensemble de méthodes et d’attributs) pour le faire. Le polymorphisme s’étend aussi à la réaction aux opérateurs (+, -, /, or, and, etc), d’autant qu’en Python, c’est implémenté avec les méthodes nommées avec __
.
Un dernier point pour les gens qui se demandent ce que veut dire overloader. L’overload est lié à l’override et au polymorphisme, mais ce n’est pas la même chose : elle consiste à overrider plusieurs fois une même méthode avec une signature différente. Il n’y a pas d’overload en Python, la notion ne nous concerne donc pas. Si vous voulez en apprendre plus, chopez un tuto Java ou C++.
Qui est qui
Avec l’héritage vient la notion de type. Quand vous créez une classe, vous créez un nouveau type. Quand vous sous-classez, vous créez un sous-type.
C’est intéressant, car une classe fille, est de son propre type ET du type de son parent (mais l’inverse n’est pas vrai).
Avec Python, on vérifie cela avec isinstance()
:
>>> isinstance(Priere(), Priere) # instance de sa propre classe True >>> isinstance(Priere(), str) # pas l'instance d'une classe quelconque False >>> isinstance(Priere(), AveMaria) # pas l'instance d'un enfant False >>> isinstance(AveMaria(), AveMaria) # instance de sa propre classe True >>> isinstance(AveMaria(), Priere) # instance de sa classe parente True
C’est utile, car certains comportements sont basés sur le type. Le plus important étant le mécanisme des exceptions. Un try
/ except
arrêtera l’exception demandée, ou du même type.
class MonExceptionPerso(Exception): pass class FillesDeMonExceptionPerso(MonExceptionPerso): pass try: raise MonExceptionPerso('Alerte ! Alerte !') except MonExceptionPerso: print 'Exception arrêtée' try: raise FillesDeMonExceptionPerso('Alerte ! Alerte !') except FillesDeMonExceptionPerso: print 'Exception arrêtée' try: raise FillesDeMonExceptionPerso('Alerte ! Alerte !') except MonExceptionPerso: print 'Exception arrêtée' try: raise MonExceptionPerso('Alerte ! Alerte !') except FillesDeMonExceptionPerso: print 'Exception arrêtée' Exception arrêtée Exception arrêtée Exception arrêtée Traceback (most recent call last): File "<ipython-input-67-7e7aec1e78c9>", line 21, in <module> raise MonExceptionPerso('Alerte ! Alerte !') MonExceptionPerso: Alerte ! Alerte !
MonExceptionPerso
est de type MonExceptionPerso
, donc l’exception est arrêtée. FillesDeMonExceptionPerso
est de type FillesDeMonExceptionPerso
, donc l’exception est arrêtée. FillesDeMonExceptionPerso
, qui hérite de MonExceptionPerso
, et donc de type MonExceptionPerso
est arrêtée.
En revanche, MonExceptionPerso
n’est PAS de type FillesDeMonExceptionPerso
. Donc elle n’est pas arrêtée.
Je le signale car il est très courant de faire des enfants de ValueError
, IOError
, IndexError
, KeyError
, etc. et de le lever dans son propre programme. Cela permet à l’utilisateur de son code de pouvoir attraper soit toutes les erreurs de son code en faisant un except
sur l’enfant, soit attraper toutes les erreurs de type ValueError
, IOError
, IndexError
, KeyError
en faisant le except
sur le parent. On laisse ainsi une marge de manoeuvre dans la gestion des erreurs.
Appeler la méthode de la classe parent
Bon, vous avez overridé une méthode du parent. Mais c’est une groooooooooooooossse méthode. Vous allez pas la réécrire en entier, si ?
class Escorte(object): def calculer_prix(self, heures, tarif): return heures * tarif class EscorteDeLuxe(Escorte): def calculer_prix(self, heures, tarif, supplement): tarif = heures * tarif return tarif + (tarif * supplement / 100)
Sur cet exemple éminement intellectuel, calculer_prix()
est simple, donc tout réécrire dans EscorteDeLuxe
n’est pas grave. Mais si c’était une Geisha
, hein ? Avec un manuel en Japonais ?
Pour éviter ces problèmes de complexité liés à la globalisation, le perméabilisation des frontières et les sites de streaming, on peut appeler la méthode du parent, dans l’enfant, en utilisant super()
:
class EscorteDeLuxe(Escorte): def calculer_prix(self, heures, tarif, supplement): # ceci est une manière compliquée de faire Escorte.calculer_prix # et de récupérer le résultat tarif = super(EscorteDeLuxe, self).calculer_prix(heures, tarif) return tarif + (tarif * supplement / 100)
C’est exactement la même chose que plus haut. Sauf qu’au lieu de copier / coller le code du parent, on l’appelle directement.
Je résume :
- on hérite du parent, et de son code
- on override son code, car on veut un comportement différent
- mais on veut quand même une partie du comportement du parent
- donc dans la méthode de l’enfant, on appelle la méthode du parent
Je réformule : quand vous overridez la méthode du parent dans l’enfant, celle du parent ne disparait pas. Elle est remplacée uniquement dans l’enfant. Et vous pouvez toujours vous servir de la version du parent en utilisant super(ClassEncours, self).nom_de_method(arguments)
si vous en avez besoin.
Bon, c’était un gros morceau, et je suis pas sûr de pas être allé trop fort. Donc laissez en comment les remarques sur ce qui pourrait être mieux expliqué. La prochaine fois, on verra des usages poussés comme la composition, la délégation et tous ces trucs d’un vrai code de production.