Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

héritage multiple

8 réponses
Avatar
n
Bonjour =E0 tous,
Je d=E9bute compl=E8tement en python (mais j'ai d'autres langages derri=E8re
moi) et j'ai lu une bonne partie du "dive into Python".
Du coup, j'ai eu envie de faire quelque chose qui est un peut chiant
avec certain langages : de l'h=E9ritage multiple.

voil=E0 un exemple qui je pense n=E9cessite un h=E9ritage multiple :
Un doctorant est =E0 la fois un Prof et un Etudiant, qui sont eux m=EAme
des Personne :

class Personne:
"definition d'un personne"
def __init__(self, nom, prenom):
self.nom =3D nom
self.prenom =3D prenom
def afficher_nom(self):
print self.nom+' '+self.prenom


class Etudiant(Personne):
"definition d'un etudiant"
def __init__(self, classe, nom, prenom):
Personne.__init__(self, nom, prenom)
self.classe =3D classe
def afficher_classe(self):
print "Classe : "+self.classe+"\n"
def afficher_statut(self):
Personne.afficher_nom(self)
self.afficher_classe()


class Prof(Personne):
"Un proffesseur"
def __init__(self, matiere, nom, prenom):
Personne.__init__(self, nom, prenom)
self.matiere =3D matiere
def afficher_matiere(self):
print "Matiere : "+self.matiere+"\n"
def afficher_statut(self):
Personne.afficher_nom(self)
self.afficher_matiere()


class Doctorant(Prof, Etudiant):
"une personne a la fois prof et etudiant"
def __init__(self, matiere, classe, nom, prenom):
Prof.__init__(self, matiere, nom, prenom)
Etudiant.__init__(self, classe, nom, prenom)
def afficher_statut(self):
print self.afficher_nom()
print self.afficher_matiere()
print self.afficher_classe()

a =3D Etudiant("3eme B", "MORIN", "Fabien")
a=2Eafficher_statut()

b =3D Prof("Math", "Gilbert", "Montanier")
b=2Eafficher_statut()

c =3D Doctorant("Math", "5e B", "Alain", "Aflelou")
c=2Eafficher_statut()

--------------------------------------
et voil=E0 la sortie :
python classe.py
MORIN Fabien
Classe : 3eme B

Gilbert Montanier
Matiere : Math

Alain Aflelou
None
Matiere : Math

None
Classe : 5e B

None
-------------------------------------
Donc =E7a marche un peut pr=E8s si on ne tiens pas compte des "None", mais
je ne pense pas que ce soit la bonne mani=E8re de proc=E9der, et je ne
trouve pas trop de documentation ou d'exemple =E0 ce sujet.

Pourriez-vous m'aider =E0 corriger ce bout de programme pour en faire
quelque chose de correcte qui utilise l'h=E9ritage multiple, ou =E0 me
rediriger vers une doc qui traite de ce probl=E8me. N'h=E9sitez pas =E0 me
d=E9tailler vos remarques/explications parce que je suis compl=E8tement
d=E9butant.

Merci beaucoup

Fab

8 réponses

Avatar
Jaypee
Bonjour à tous,
Je débute complètement en python (mais j'ai d'autres langages derrière
moi) et j'ai lu une bonne partie du "dive into Python".
Du coup, j'ai eu envie de faire quelque chose qui est un peut chiant
avec certain langages : de l'héritage multiple.

voilà un exemple qui je pense nécessite un héritage multiple :
Un doctorant est à la fois un Prof et un Etudiant, qui sont eux même
des Personne :

...


Merci beaucoup

Fab

Dans le design objet de tous les jours, on a pris un peu de distance

avec l'héritage multiple, qui introduit des dépendances généralement
plus rigides que celles souhaitées.

On remplace généralement les relations "estUn" par des relations "aUn"

Et dans les langages comme Python çà se traduit par l' utilisation de
"Mixins" encapsulés dans des Modules.

Un Doctorant est en effet une personne ayant à la fois un Comportement
de prof et d'étudiant.

Dans cette approche, il faut alors une classe Personne, et deux mixins,
Comportement de Prof et Comportement d'Etudiant.

J-P

Avatar
n
ok, cela n'est pas très parlant pour moi.
Je pense que je vais essayer des exemple plus simples.

Sinon, sur le bout de code que j'ai posté,qu'est ce qu'on peut
redire ?

Fab
Avatar
Jaypee
ok, cela n'est pas très parlant pour moi.
Je pense que je vais essayer des exemple plus simples.

Sinon, sur le bout de code que j'ai posté,qu'est ce qu'on peut
redire ?

Fab

Comment décrirait-on un doctorant qui n'a plus de cours à suivre, ou

celui qui ne donne pas encore de cours?

Ton design n'est pas assez souple.

Un mixin sert à "avoir" les comportements d'un objet sans pour autant
"être" un dérivé de cet objet.

Par exemple, je suis une personne. Mais lorsque c'est nécessaire je
deviens, une "personne dans une file d'attente", et je comprends alors
"personne devant moi" ou "personne derrière moi".
Pour autant, toute ma vie n'est pas une liste d'attente. Je peux
"oublier" ces comportements d'emprunt une fois sorti de la file.

C'est la même idée pour les objets, ils n'ont pas besoin d'hériter
éternellement de certains comportements. On peut les ajouter en fonction
du besoin.


J-P

Avatar
maric
Bonjour à tous,
Je débute complètement en python (mais j'ai d'autres langages derrière
moi) et j'ai lu une bonne partie du "dive into Python".
Du coup, j'ai eu envie de faire quelque chose qui est un peut chiant
avec certain langages : de l'héritage multiple.


Bienvenue !


voilà un exemple qui je pense nécessite un héritage multiple :
Un doctorant est à la fois un Prof et un Etudiant, qui sont eux même
des Personne :



Quelques commentaires donc,

class Personne:


Utilisez les classes "new-style", systématiquement !

class Personne(object)

"definition d'un personne"


Bon on va dire que c'est un exercice mais une docstring est censée
ajouter de l'information...

def __init__(self, nom, prenom):
self.nom = nom
self.prenom = prenom
def afficher_nom(self):
print self.nom+' '+self.prenom


class Etudiant(Personne):
"definition d'un etudiant"
def __init__(self, classe, nom, prenom):
Personne.__init__(self, nom, prenom)
self.classe = classe
def afficher_classe(self):
print "Classe : "+self.classe+"n"
def afficher_statut(self):
Personne.afficher_nom(self)
self.afficher_classe()


Ah ! oui, c'est bien les noms des classes et méthides suivent les
recommandations de la PEP 8, je ne sais pas si c'est volontaire, au cas où :
http://www.python.org/dev/peps/pep-0008/
Je ne saia pas non plus si c'est une histoire de copier-coller mais la
recommandation concernant l'indentation et de quatre espaces par niveau
(pas de tabulation).



class Prof(Personne):
"Un proffesseur"
def __init__(self, matiere, nom, prenom):
Personne.__init__(self, nom, prenom)
self.matiere = matiere
def afficher_matiere(self):
print "Matiere : "+self.matiere+"n"
def afficher_statut(self):
Personne.afficher_nom(self)
self.afficher_matiere()


class Doctorant(Prof, Etudiant):
"une personne a la fois prof et etudiant"
def __init__(self, matiere, classe, nom, prenom):
Prof.__init__(self, matiere, nom, prenom)
Etudiant.__init__(self, classe, nom, prenom)
def afficher_statut(self):
print self.afficher_nom()


pourquoi print ? afficher_xxx retourne None (valeur de retour par défaut
d'une fonction), donc l'instruction print va afficher "None".

print self.afficher_matiere()
print self.afficher_classe()

a = Etudiant("3eme B", "MORIN", "Fabien")
a.afficher_statut()

b = Prof("Math", "Gilbert", "Montanier")
b.afficher_statut()

c = Doctorant("Math", "5e B", "Alain", "Aflelou")
c.afficher_statut()

--------------------------------------
et voilà la sortie :
python classe.py
MORIN Fabien
Classe : 3eme B

Gilbert Montanier
Matiere : Math

Alain Aflelou
None
Matiere : Math

None
Classe : 5e B

None
-------------------------------------
Donc ça marche un peut près si on ne tiens pas compte des "None", mais


cf. plus haut.

je ne pense pas que ce soit la bonne manière de procéder, et je ne
trouve pas trop de documentation ou d'exemple à ce sujet.



Comme signalé dans l'autre post, il est vrai que l'héritage (tout court,
ce n'est pas spécifique à l'héritage multiple), bien que parfait en
théorie (définition de la relation "est un") est souvent moins flexible
que l'agrégation (relation "à un"). Il faut se souvenir du Zen of Python
(import this), "flat is better than nested" (plat c'est mieux
qu'imbriqué), sentence qui s'applique aussi bien à l'architecture des
modules qu'à l'arborescence des classes.

On ne peut laisser entendre cependant qu'il faut proscrire l'héritage
(simple ou multiple), par exemple, dans votre cas, si cela doit avoir un
sens de dire isinstance(aDoctorant, Personne), il faut l'utiliser.

Il y a en revanche un problème spécifique à l'héritage multiple, qui se
nomme "problème du diamant".

Suivant l'arborescence de vos classes, lorsque vous appelez la méthode
__init__ du doctorant la méthode __init__ de Personne sera appelée deux
fois. Une fois par la méthode __init__ de Prof et l'autre fois par celle
d'Etudiant. Ici, cela ne porte pas à conséquence mais il se peut que
cela devienne très problématique et oblige plus tard à un refactoring de
toute l'arborescence.

Python offre un moyen pour remédier à ce problème, ce sont les méthodes
collaboratives qu'on définies avec l'utilisation de super (je vous
laisse regarder dans la doc, l'explication du mécanisme est un peu
compliquée et a à voir avec le mro, voir
http://www.python.org/download/releases/2.2.2/descrintro/#cooperation).

L'idée de base c'est de garantir que l'appel aux méthodes de chacune des
classes de l'arborescence ne se fera qu'une seule fois.

In [2]: class A(object) :
...: def collab(self) :
...: print 'A'
...:
...:

In [3]: class B(A) :
...: def collab(self) :
...: super(B, self).collab()
...: print 'B'
...:
...:

In [4]: class C(A) :
...: def collab(self) :
...: super(C, self).collab()
...: print 'C'
...:
...:

In [5]: class D(B, C) :
...: def collab(self) :
...: super(D, self).collab()
...: print 'D'

In [7]: B().collab()
A
B

In [9]: C().collab()
A
C

In [8]: D().collab()
A
C
B
D


La contre-partie c'est que vous n'êtes pas sûr que l'appel à
super(Etudiant, self).__init__ soit toujours celui de la classe
Personne. On le voit dans l'exemple précédent lorsque super(B,
self).collab() appel dans un cas A.collab et dans l'autre C.collab.
C'est encore plus évident si je défini la classe E de la façon suivante:


In [12]: class E(C, B) :
....: def collab(self) :
....: super(E, self).collab()
....: print 'E'
....:
....:

In [13]: D().collab()
A
C
B
D

In [14]: E().collab()
A
B
C
E

La conclusion c'est que vous ne pouvez faire aucune supposition sur les
paramètres attendus par la méthode parente appelée, donc il faut
absolument repecté une règle d'or pour les méthodes collaboratives : -
toutes les méthodes doivent avoir la même suite d'arguments positionels
que la méthode de base,
- elle doivent toutes accepter un dictionnaire d'arguments optionels,
- tout argument n'appartenant à la liste commune lors d'un appel à une
méthode collaborative doit être nommé (l'idéal est d'avoir une valeur
par défaut pour les arguments supplémentaires).

Voilà comment réécrire votre hiérarchie avec ce principe, en ne prenant
en compte que la méthode __init__ :

class Personne(object):
"Un personne est un objet qui ...."
def __init__(self, nom, prenom, **kwargs):
"""Cette méthode est collaborative, tout argument autre que les deux
premiers doivent être nommés"""
self.nom = nom
self.prenom = prenom

class Etudiant(Personne):
def __init__(self, nom, prenom, classe, **kwargs):
super(Etudiant, self).__init__(nom, prenom, **kwargs)
self.classe = classe

class Prof(Personne):
def __init__(self, nom, prenom, matiere, **kwargs):
super(Prof, self).__init__(nom, prenom, **kwargs)
self.matiere = matiere

class Doctorant(Prof, Etudiant):
def __init__(self, nom, prenom, classe, matiere, **kwargs):
super(Doctorant, self).__init__(nom, prenom,
matiere=matiere,
classe=classe, **kwargs)


voilà, maintenant vous pouvez faire :

napster = Doctorant('smith', 'napster', 'info 3e cycle', 'Python')
napster.__dict__
{'matiere': 'Python', 'nom': 'smith', 'classe': 'info 3e cycle',



'prenom': 'napster'}
isinstance(napster, Etudiant)
True




etc.

Le grand intérêt de super est de pouvoir profiter pleinement de
l'extension d'implémentation d'une méthode. Ceci dit il ne faut pas
pousser ce raisonnement trop loin. Un contre exemple :

Admettons qu'on ai défini la méthode nom_complet dans la classe Personne
de la façon suivante:


def nom_complet(self) :
return self.nom + ', ' + self.prenom


et la méthode affiche_statut dans la classe Doctorant :

def affiche_statut(self) :
print self.nom_complet()
print self.matiere
print self.classe

Alors la définition des classes suivantes produit un résultat
particulièrement intéressant:

class EtudiantEtranger(Etudiant) :

def __init__(self, nom, prenom, pays, **kwargs):
super(EtudiantEtranger, self).__init__(nom, prenom, **kwargs)
self.pays = pays

def nom_complet(self) :
return (super(EtudiantEtranger, self).nom_complet()
+ ' (' + self.pays + ')')

class EtudiantTravailleur(Etudiant) :

def nom_complet(self) :
return (super(EtudiantTravailleur, self).nom_complet()
+ ' (dispense)')

class DoctorantEtranger(Doctorant, EtudiantEtranger) : pass
class DoctorantEtrangerTravailleur(DoctorantEtranger,
EtudiantTravailleur) : pass

igor = DoctorantEtranger('Ilitch', 'igor', classe='info 3e cycle',
matiere='



Python', pays='Russie')
igor.affiche_statut()
Ilitch, igor (Russie)



Python
info 3e cycle
igor = DoctorantEtrangerTravailleur('Ilitch', 'igor', classe='info
3e cycle'



, matiere='Python', pays='Russie')
igor.affiche_statut()
Ilitch, igor (dispense) (Russie)



Python
info 3e cycle


Le même résultat est moins immédiat avec l'utilisation de mixins. Ceci
dit, ce schéma peut vous amener produire un grand nombre de classe, et
on sait par expérience que les grosses hiérarchies sont lourdes à
manipuler, il est donc souvent plus judicieux de transformer ce qui peut
l'être en attributs d'une classe plutôt qu'en hértitage. Dans mon
dernier exemple, "Etranger" et "Travailleur" devrait être des attributs
d'Etudiant (ou Personne), et à charge à la méthode nom_complet de cette
classe de gérer les différents cas.
L'utilisation de mixins découle du même principe, et offre d'autres
problèmes (elle n'est pas magiquement substituable à l'héritage), elle
utilise beaucoup en python ce que l'on appelle la délégation, mais c'est
une autre histoire...



Merci beaucoup



de rien, c'est un plaisir, je crois que je vais en profiter pour
compléter le chapitre sur les méthodes collaboratives dans mon support
de cours.


--
Maric Michaud



Avatar
n
merci pour cette réponse très claire et très complète, malgré mon
niveau de débutant, j'ai parfaitement compris ce que tu m'a exliqué.
Pour le coding style j'ai fait ça un peut au hasard, et selon des
exemples que j'ai dû voir trainer sur le net, mais tu confirme qu'il
faut bien mettre des espaces et pas des tabulations ? (j'ai cru
comprendre que c'était un sujet de guerre dans certain langage, pas en
python ?)


Bonjour à tous,
Je débute complètement en python (mais j'ai d'autres langages derr ière
moi) et j'ai lu une bonne partie du "dive into Python".
Du coup, j'ai eu envie de faire quelque chose qui est un peut chiant
avec certain langages : de l'héritage multiple.


Bienvenue !


voilà un exemple qui je pense nécessite un héritage multiple :
Un doctorant est à la fois un Prof et un Etudiant, qui sont eux mê me
des Personne :



Quelques commentaires donc,

class Personne:


Utilisez les classes "new-style", systématiquement !

class Personne(object)

"definition d'un personne"


Bon on va dire que c'est un exercice mais une docstring est censée
ajouter de l'information...

def __init__(self, nom, prenom):
self.nom = nom
self.prenom = prenom
def afficher_nom(self):
print self.nom+' '+self.prenom


class Etudiant(Personne):
"definition d'un etudiant"
def __init__(self, classe, nom, prenom):
Personne.__init__(self, nom, prenom)
self.classe = classe
def afficher_classe(self):
print "Classe : "+self.classe+"n"
def afficher_statut(self):
Personne.afficher_nom(self)
self.afficher_classe()


Ah ! oui, c'est bien les noms des classes et méthides suivent les
recommandations de la PEP 8, je ne sais pas si c'est volontaire, au cas o ù :
http://www.python.org/dev/peps/pep-0008/
Je ne saia pas non plus si c'est une histoire de copier-coller mais la
recommandation concernant l'indentation et de quatre espaces par niveau
(pas de tabulation).



class Prof(Personne):
"Un proffesseur"
def __init__(self, matiere, nom, prenom):
Personne.__init__(self, nom, prenom)
self.matiere = matiere
def afficher_matiere(self):
print "Matiere : "+self.matiere+"n"
def afficher_statut(self):
Personne.afficher_nom(self)
self.afficher_matiere()


class Doctorant(Prof, Etudiant):
"une personne a la fois prof et etudiant"
def __init__(self, matiere, classe, nom, prenom):
Prof.__init__(self, matiere, nom, prenom)
Etudiant.__init__(self, classe, nom, prenom)
def afficher_statut(self):
print self.afficher_nom()


pourquoi print ? afficher_xxx retourne None (valeur de retour par défaut
d'une fonction), donc l'instruction print va afficher "None".

print self.afficher_matiere()
print self.afficher_classe()

a = Etudiant("3eme B", "MORIN", "Fabien")
a.afficher_statut()

b = Prof("Math", "Gilbert", "Montanier")
b.afficher_statut()

c = Doctorant("Math", "5e B", "Alain", "Aflelou")
c.afficher_statut()

--------------------------------------
et voilà la sortie :
python classe.py
MORIN Fabien
Classe : 3eme B

Gilbert Montanier
Matiere : Math

Alain Aflelou
None
Matiere : Math

None
Classe : 5e B

None
-------------------------------------
Donc ça marche un peut près si on ne tiens pas compte des "None", mais


cf. plus haut.

je ne pense pas que ce soit la bonne manière de procéder, et je ne
trouve pas trop de documentation ou d'exemple à ce sujet.



Comme signalé dans l'autre post, il est vrai que l'héritage (tout cou rt,
ce n'est pas spécifique à l'héritage multiple), bien que parfait en
théorie (définition de la relation "est un") est souvent moins flexib le
que l'agrégation (relation "à un"). Il faut se souvenir du Zen of Pyt hon
(import this), "flat is better than nested" (plat c'est mieux
qu'imbriqué), sentence qui s'applique aussi bien à l'architecture des
modules qu'à l'arborescence des classes.

On ne peut laisser entendre cependant qu'il faut proscrire l'héritage
(simple ou multiple), par exemple, dans votre cas, si cela doit avoir un
sens de dire isinstance(aDoctorant, Personne), il faut l'utiliser.

Il y a en revanche un problème spécifique à l'héritage multiple, qui se
nomme "problème du diamant".

Suivant l'arborescence de vos classes, lorsque vous appelez la méthode
__init__ du doctorant la méthode __init__ de Personne sera appelée de ux
fois. Une fois par la méthode __init__ de Prof et l'autre fois par celle
d'Etudiant. Ici, cela ne porte pas à conséquence mais il se peut que
cela devienne très problématique et oblige plus tard à un refactori ng de
toute l'arborescence.

Python offre un moyen pour remédier à ce problème, ce sont les mé thodes
collaboratives qu'on définies avec l'utilisation de super (je vous
laisse regarder dans la doc, l'explication du mécanisme est un peu
compliquée et a à voir avec le mro, voir
http://www.python.org/download/releases/2.2.2/descrintro/#cooperation).

L'idée de base c'est de garantir que l'appel aux méthodes de chacune des
classes de l'arborescence ne se fera qu'une seule fois.

In [2]: class A(object) :
...: def collab(self) :
...: print 'A'
...:
...:

In [3]: class B(A) :
...: def collab(self) :
...: super(B, self).collab()
...: print 'B'
...:
...:

In [4]: class C(A) :
...: def collab(self) :
...: super(C, self).collab()
...: print 'C'
...:
...:

In [5]: class D(B, C) :
...: def collab(self) :
...: super(D, self).collab()
...: print 'D'

In [7]: B().collab()
A
B

In [9]: C().collab()
A
C

In [8]: D().collab()
A
C
B
D


La contre-partie c'est que vous n'êtes pas sûr que l'appel à
super(Etudiant, self).__init__ soit toujours celui de la classe
Personne. On le voit dans l'exemple précédent lorsque super(B,
self).collab() appel dans un cas A.collab et dans l'autre C.collab.
C'est encore plus évident si je défini la classe E de la façon suiv ante:


In [12]: class E(C, B) :
....: def collab(self) :
....: super(E, self).collab()
....: print 'E'
....:
....:

In [13]: D().collab()
A
C
B
D

In [14]: E().collab()
A
B
C
E

La conclusion c'est que vous ne pouvez faire aucune supposition sur les
paramètres attendus par la méthode parente appelée, donc il faut
absolument repecté une règle d'or pour les méthodes collaboratives : -
toutes les méthodes doivent avoir la même suite d'arguments positione ls
que la méthode de base,
- elle doivent toutes accepter un dictionnaire d'arguments optionels,
- tout argument n'appartenant à la liste commune lors d'un appel à une
méthode collaborative doit être nommé (l'idéal est d'avoir une va leur
par défaut pour les arguments supplémentaires).

Voilà comment réécrire votre hiérarchie avec ce principe, en ne p renant
en compte que la méthode __init__ :

class Personne(object):
"Un personne est un objet qui ...."
def __init__(self, nom, prenom, **kwargs):
"""Cette méthode est collaborative, tout argument autre que les deux
premiers doivent être nommés"""
self.nom = nom
self.prenom = prenom

class Etudiant(Personne):
def __init__(self, nom, prenom, classe, **kwargs):
super(Etudiant, self).__init__(nom, prenom, **kwargs)
self.classe = classe

class Prof(Personne):
def __init__(self, nom, prenom, matiere, **kwargs):
super(Prof, self).__init__(nom, prenom, **kwargs)
self.matiere = matiere

class Doctorant(Prof, Etudiant):
def __init__(self, nom, prenom, classe, matiere, **kwargs):
super(Doctorant, self).__init__(nom, prenom,
matiere=matiere,
classe=classe, **kwargs)


voilà, maintenant vous pouvez faire :

napster = Doctorant('smith', 'napster', 'info 3e cycle', 'Python')
napster.__dict__
{'matiere': 'Python', 'nom': 'smith', 'classe': 'info 3e cycle',



'prenom': 'napster'}
isinstance(napster, Etudiant)
True




etc.

Le grand intérêt de super est de pouvoir profiter pleinement de
l'extension d'implémentation d'une méthode. Ceci dit il ne faut pas
pousser ce raisonnement trop loin. Un contre exemple :

Admettons qu'on ai défini la méthode nom_complet dans la classe Perso nne
de la façon suivante:


def nom_complet(self) :
return self.nom + ', ' + self.prenom


et la méthode affiche_statut dans la classe Doctorant :

def affiche_statut(self) :
print self.nom_complet()
print self.matiere
print self.classe

Alors la définition des classes suivantes produit un résultat
particulièrement intéressant:

class EtudiantEtranger(Etudiant) :

def __init__(self, nom, prenom, pays, **kwargs):
super(EtudiantEtranger, self).__init__(nom, prenom, **kwargs)
self.pays = pays

def nom_complet(self) :
return (super(EtudiantEtranger, self).nom_complet()
+ ' (' + self.pays + ')')

class EtudiantTravailleur(Etudiant) :

def nom_complet(self) :
return (super(EtudiantTravailleur, self).nom_complet()
+ ' (dispense)')

class DoctorantEtranger(Doctorant, EtudiantEtranger) : pass
class DoctorantEtrangerTravailleur(DoctorantEtranger,
EtudiantTravailleur) : pass

igor = DoctorantEtranger('Ilitch', 'igor', classe='info 3e cycle ',
matiere='



Python', pays='Russie')
igor.affiche_statut()
Ilitch, igor (Russie)



Python
info 3e cycle
igor = DoctorantEtrangerTravailleur('Ilitch', 'igor', classe='in fo
3e cycle'



, matiere='Python', pays='Russie')
igor.affiche_statut()
Ilitch, igor (dispense) (Russie)



Python
info 3e cycle


Le même résultat est moins immédiat avec l'utilisation de mixins. C eci
dit, ce schéma peut vous amener produire un grand nombre de classe, et
on sait par expérience que les grosses hiérarchies sont lourdes à
manipuler, il est donc souvent plus judicieux de transformer ce qui peut
l'être en attributs d'une classe plutôt qu'en hértitage. Dans mon
dernier exemple, "Etranger" et "Travailleur" devrait être des attributs
d'Etudiant (ou Personne), et à charge à la méthode nom_complet de c ette
classe de gérer les différents cas.
L'utilisation de mixins découle du même principe, et offre d'autres
problèmes (elle n'est pas magiquement substituable à l'héritage), e lle
utilise beaucoup en python ce que l'on appelle la délégation, mais c' est
une autre histoire...



Merci beaucoup



de rien, c'est un plaisir, je crois que je vais en profiter pour
compléter le chapitre sur les méthodes collaboratives dans mon support
de cours.


--
Maric Michaud





Avatar
Bruno Desthuilliers
Bonjour à tous,
Je débute complètement en python (mais j'ai d'autres langages derrière
moi) et j'ai lu une bonne partie du "dive into Python".
Du coup, j'ai eu envie de faire quelque chose qui est un peut chiant
avec certain langages : de l'héritage multiple.

voilà un exemple qui je pense nécessite un héritage multiple :
Un doctorant est à la fois un Prof et un Etudiant, qui sont eux même
des Personne :


<hs devrait-aller-sur="fr.comp.objet">
Ca dépend bien sûr du système à concevoir, mais mon expérience de
l'informatique de gestion est que moins on a de couplage dans les
classes métier, et mieux on se porte. Or, l'héritage induit un couplage
assez fort, même dans un langage aussi dynamique que Python.

Juste à titre d'exemple, comment modélise-tu, dans ton système, le cas
d'un Etudiant qui, après avoir obtenu ses diplômes, est embauché comme
Professeur ? Avec une relation 'est un', tu va devoir créer une nouvelle
instance de Professeur, et recopier les attributs de l'instance de
Etudiant. Ce qui pose (au moins) deux problèmes : d'une part, une
duplication de données (puisqu'a priori, dans un système d'information,
on conserve les 'archives' durant un certain temps), et d'autre part une
non-identité entre deux instances de Personne représentant la même personne.

Bref (et pour surenchérir sur Maric), la modélisation 'correcte' (NB:
relire la toute première proposition de ce post et nuancer en
fonction...) est d'utiliser une notion de rôle. Etudiant et professeur
sont deux des (potentiellement nombreux) rôles qu'une même personne peut
jouer dans sa vie - parfois simultanément, comme par exemple dans le cas
des doctorants. Ce n'est donc pas une relation 'est un' (puisque cette
relation peut évoluer durant le cycle de vie d'un objet Personne), mais
'a un' - ce qui se traduit généralement par une solution du type
[composition ou aggrégation] + délégation.
</hs>

Voilà pour la théorie. Comme le but de ton exo était de jouer avec
l'héritage multiple, on va faire comme si le modèle était ok...


class Personne:


# rends toi un service: utilise les classes 'new-style' (qui ne sont pas
si 'new' que ça, puisque c'est le modèle objet recommendé depuis la 2.2
qui date de 2001)

class Personne(object):


"definition d'un personne"


Argh. Vire ces tabs et remplace les par des espaces, par pitié.

def __init__(self, nom, prenom):
self.nom = nom
self.prenom = prenom
def afficher_nom(self):
print self.nom+' '+self.prenom


L'affichage ne devrait pas être une responsabilité d'une classe métier.
Surcharge plutôt la méthode __repr__ et/ou la méthode __str__.

def __str__(self):
return "%(prenom)s %(nom)s" % self.__dict__

def __repr__(self):
return "<%s {%s} at %xd>"
% (str(type(self)).strip('<>'), str(self), id(self)

Après, tu peux soit:
p =Personne('Morin', 'Fabien')
p
<class '__main__.Personne' {Fabien Morin} at 4032ef6cd>



print p
Fabien Morin








class Etudiant(Personne):
"definition d'un etudiant"
def __init__(self, classe, nom, prenom):


Usuellement, quand une méthode surchargée prend plus d'argument que
l'original, on place ces arguments après. Ta signature gagnerait donc à
être:
def __init__(self, nom, prenom, classe):

Personne.__init__(self, nom, prenom)


Dans la mesure où tu utilises maintenant une classe new-style, tu peux
aussi utiliser super():
super(Etudiant, self).__init__(nom, prenom)

L'intérêt est que celà évite de répéter le codage en dur du nom de la
superclasse (répétitionce qui viole la règle 'DRY' ("Don't Repeat
Yourself") et sa soeur la règle 'SPOT' ("Single Point Of
Transformation"). Maintenannt, si tu veux introduire une autre classe
(compatible, of course) dans la hiérarchie entre Personne et Etudiant,
tu n'a que la liste des classes de base à modifier.

L'"inconvénient" de super() est que les signatures des méthodes
surchargées doivent être compatibles. Je mets "inconvénient" entre
guillements parce que, théoriquement, cette compatibilité devrait être
assurée, du moins si on veux respecter le principe de substitution de
Liskov. Dans la pratique, il est avantageux, si on veut aller dans cette
direction, de prévoir le coup en ajoutant systématiquement *args et
**kwargs aux signatures des méthodes en question pour supporter par
avance tout paramètre supplémentaire. Et comme on ne peut pas forcément
savoir à l'avance comment et dans quel ordre ces méthodes seront
appelées[1], on passera ces paramètres à super() comme arguments nommés:

[1] réécris ce code en passant ces arguments par position lors de
l'appel à super(), et tu constateras quelques dysfonctionnements une
fois arrivé à la classe Doctorant...

On aurait donc:
class Personne(object):
def __init__(self, nom, prenom, *args, **kwargs):
self.nom = nom
self.prenom = prenom

def __str__(self):
return "%(prenom)s %(nom)s" % self.__dict__

def __repr__(self):
return "<%s {%s} at %xd>"
% (str(type(self)).strip('<>'), str(self), id(self))

class Etudiant(Personne):
def __init__(self, nom, prenom, classe, *args, **kwargs):
super(Etudiant, self).__init__(nom, prenom,
classe=classe,
*args, **kwargs)
self.classe = classe

def __str__(self):
return "%s - classe: %s"
% (super(Etudiant, self).__str__(), self.classe)


On voit aussi que le formattage de la représentation commence à tourner
au bricolage. On peut donc déjà envisager une petite refactorisation:

class Personne(object):
def __init__(self, nom, prenom, *args, **kwargs):
self.nom = nom
self.prenom = prenom

def _get_fmt_string(self):
return "%(nom)s %(prenom)s"

def __str__(self):
return self._get_fmt_string() % self.__dict__

def __repr__(self):
return "<%s {%s} at %xd>"
% (str(type(self)).strip('<>'), str(self), id(self))

class Etudiant(Personne):
def __init__(self, nom, prenom, classe, *args, **kwargs):
super(Etudiant, self).__init__(nom, prenom,
classe=classe,
*args, **kwargs)
self.classe = classe

def _get_fmt_string(self):
return super(Etudiant, self)._get_fmt_string()
+ " - classe: %(classe)s"
(snip)



class Prof(Personne):
"Un proffesseur"


Heu... Déjà que la docstring n'apporte rien par rapport au nom de la
classe...

class Professeur(Personne)

def __init__(self, matiere, nom, prenom):
Personne.__init__(self, nom, prenom)
self.matiere = matiere
def afficher_matiere(self):
print "Matiere : "+self.matiere+"n"
def afficher_statut(self):
Personne.afficher_nom(self)


Tu n'a pas besoin d'appeler explicitement la méthode de la classe parent
ici.
self.afficher_nom()

self.afficher_matiere()



Même punition, même motif:

class Professeur(Personne):
def __init__(self, nom, prenom, matiere, *args, **kwargs):
super(Professeur, self).__init__(nom, prenom,
matiere=matiere,
*args, **kwargs)
self.matiere = matiere

def _get_fmt_string(self):
return super(Professeur, self)._get_fmt_string()
+ " - matière: %(matiere)s"





class Doctorant(Prof, Etudiant):


Et on arrive là aux limites de l'exercice...

"une personne a la fois prof et etudiant"
def __init__(self, matiere, classe, nom, prenom):
Prof.__init__(self, matiere, nom, prenom)
Etudiant.__init__(self, classe, nom, prenom)


Tu initialise deux fois nom et prénom. Ici, ce n'est pas bien grave,
mais dans la réalité il arrive que certaines initialisations impliquent
des calculs trop coûteux pour que ce soit acceptable.

def afficher_statut(self):
print self.afficher_nom()
print self.afficher_matiere()
print self.afficher_classe()


Et là, tu es obliger de dupliquer le code des méthodes 'afficher_statut'
des deux classes parentes... Ce qui viole quelque peu l'encapsulation.

Voyons un peu avec les classes new-style, super() et des signatures
compatibles:

class Doctorant(Etudiant, Prof):
def __init__(self, nom, prenom, classe, matiere, *args, **kwargs):
super(Doctorant, self).__init__(nom, prenom,
classe=classe,
matiere=matiere,
*args, **kwargs)



d = Doctorant("Einstein", "Albert", "terminale", "Sciences")
d
<class '__main__.Doctorant' {Einstein Albert - matière: Sciences -



classe: terminale} at 407716acd>

Et oh, miracle, ça donne le bon résultat - sans mettre les mains dans le
cambouis, !-)

A vrai dire, ça marche même avec encore moins de code:
class Doctorant2(Professeur, Etudiant):
pass

d2 = Doctorant2('Marx', 'Groucho', 'absurdité', 'maternelle')
d2

<class '__main__.Doctorant2' {Marx Groucho - classe: maternelle -


matière: absurdité} at 4077162cd>






mais
je ne pense pas que ce soit la bonne manière de procéder, et je ne
trouve pas trop de documentation ou d'exemple à ce sujet.


Comme tu peux le constater avec l'exemple ci-dessus, utiliser
correctement l'héritage multiple dans une même hiérarchie de classes
est (bien sûr) possible mais demande un peu de réflexion préalable et
impose certaines limitations (du moins si on veut avoir quelque chose de
propre et maintenable - on peut bien sûr aussi faire goret...). De plus,
comme on l'a vu en préambule, ce cas de figure (héritage multiple dans
une même hiérarchie) est assez souvent le signe d'une erreur d'analyse
et de conception.

Le fait est qu'en Python, on utilise moins l'héritage que d'autres
langages, et on tend à restreindre l'utilisation de l'héritage multiple
aux classes mixins (des classes destinées à ajouter des fonctionalités
'pratiques' à d'autres classes - c'est une technique très utilisée dans
Zope2 pour ajouter aux classes 'métier' toute la plomberie nécessaire
pour fonctionner dans Zope).

Dans la pratique, et en ce qui me concerne, je ne crois pas avoir jamais
utilisé l'héritage multiple autrement que pour des mixins.

HTH



Avatar
BertrandB

Juste à titre d'exemple, comment modélise-tu, dans ton système, le cas
d'un Etudiant qui, après avoir obtenu ses diplômes, est embauché comme
Professeur ? Avec une relation 'est un', tu va devoir créer une nouvelle
instance de Professeur, et recopier les attributs de l'instance de
Etudiant. Ce qui pose (au moins) deux problèmes : d'une part, une
duplication de données (puisqu'a priori, dans un système d'information,
on conserve les 'archives' durant un certain temps), et d'autre part une
non-identité entre deux instances de Personne représentant la même
personne.

Bref (et pour surenchérir sur Maric), la modélisation 'correcte' (NB:
relire la toute première proposition de ce post et nuancer en
fonction...) est d'utiliser une notion de rôle. Etudiant et professeur
sont deux des (potentiellement nombreux) rôles qu'une même personne peut
jouer dans sa vie - parfois simultanément, comme par exemple dans le cas
des doctorants. Ce n'est donc pas une relation 'est un' (puisque cette
relation peut évoluer durant le cycle de vie d'un objet Personne), mais
'a un' - ce qui se traduit généralement par une solution du type
[composition ou aggrégation] + délégation.


bjr,

N'étant pas un maître de l'objet mais ayant joué avec un langage acteur
je vois l'avantage de ces relations mais je ne vois pas comment l'écrire
pouvez vous (la cantonade) éclairer ma lanterne ?

Avatar
Bruno Desthuilliers



Juste à titre d'exemple, comment modélise-tu, dans ton système, le cas
d'un Etudiant qui, après avoir obtenu ses diplômes, est embauché comme
Professeur ? Avec une relation 'est un', tu va devoir créer une
nouvelle instance de Professeur, et recopier les attributs de
l'instance de Etudiant. Ce qui pose (au moins) deux problèmes : d'une
part, une duplication de données (puisqu'a priori, dans un système
d'information, on conserve les 'archives' durant un certain temps), et
d'autre part une non-identité entre deux instances de Personne
représentant la même personne.

Bref (et pour surenchérir sur Maric), la modélisation 'correcte' (NB:
relire la toute première proposition de ce post et nuancer en
fonction...) est d'utiliser une notion de rôle. Etudiant et professeur
sont deux des (potentiellement nombreux) rôles qu'une même personne
peut jouer dans sa vie - parfois simultanément, comme par exemple dans
le cas des doctorants. Ce n'est donc pas une relation 'est un'
(puisque cette relation peut évoluer durant le cycle de vie d'un objet
Personne), mais 'a un' - ce qui se traduit généralement par une
solution du type [composition ou aggrégation] + délégation.



bjr,

N'étant pas un maître de l'objet mais ayant joué avec un langage acteur
je vois l'avantage de ces relations mais je ne vois pas comment l'écrire


Ca dépend du langage et du contexte. Une solution courante est
d'appliquer le pattern 'decorator', mais ça peut être incomplet si par
exemple on veut connaitre les roles d'une Personne donnée. Une recherche
sur "role object pattern" devrait te retourner plus d'infos.

pouvez vous (la cantonade) éclairer ma lanterne ?