OVH Cloud OVH Cloud

Heritage de methode

31 réponses
Avatar
Francois
Bonjour,

J'ai une classe 'Vecteur' qui m'intéresse et à laquelle je souhaite
rajouter quelques méthodes.
Comme cette classe est 'figée' et que je ne peux pas y toucher, j'ai
pensé créer une classe 'MonVecteur' qui dérive de cette classe et à
laquelle je rajoute mes propres méthodes.

En particulier je souhaiterais néanmoins que 'MonVecteur' dispose des
mêmes méthodes que 'Vecteur', pour les additions, les
multiplications, les normalisations, etc...
Le problème est que ces méthodes _créent_ un objet de classe 'Vecteur'
et non pas 'MonVecteur' lorsque j'essaye par exemple d'additioner deux
objets de classe 'MonVecteur'.

J'espère avoir été clair, donc si quelqu'un avait une idée pour
résoudre mon problème...

Merci d'avance,

10 réponses

1 2 3 4
Avatar
bruno at modulix
Christophe wrote:

(snip)



BTW, j'en profite pour recommander la BonnePratique(tm) suivante : ne
pas coder le nom de la classe en dur dans le code. Christophe n'aurait
pas ce problème si scale() avait été implémenté ainsi:

def scale(self, scale):
return self.__class__(self.x*scale, self.y*scale, self.z*scale)



Facile à dire.



Facile à faire aussi dans *tes* développements !-)

Ce n'était pas une solution à ton problème, mais une pratique qui, si
elle avait été suivie par l'auteur de ta classe Vecteur, t'aurais évité
le problème que tu a maintenant à résoudre.

--
bruno desthuilliers
python -c "print '@'.join(['.'.join([w[::-1] for w in p.split('.')]) for
p in ''.split('@')])"


Avatar
Christophe Cavalaria
bruno at modulix wrote:

Christophe wrote:

(snip)



BTW, j'en profite pour recommander la BonnePratique(tm) suivante : ne
pas coder le nom de la classe en dur dans le code. Christophe n'aurait
pas ce problème si scale() avait été implémenté ainsi:

def scale(self, scale):
return self.__class__(self.x*scale, self.y*scale, self.z*scale)



Facile à dire.



Facile à faire aussi dans *tes* développements !-)

Ce n'était pas une solution à ton problème, mais une pratique qui, si
elle avait été suivie par l'auteur de ta classe Vecteur, t'aurais évité
le problème que tu a maintenant à résoudre.


Oui mais c'est pas évident de penser à ça.

En conclusion quand même, on peut noter que ce genre de situation est moins
bloquante en Python que dans la pluspart des langages objets. Il y a
plusieurs solutions possibles qui ne nécessitent pas trop d'entretient
( modifier directement Vector, encapsuler Vector et faire un passthrough
avec __getattr__ et __setattr__. Il reste quand même le problème du code
externe qui peut retourner des nouveaux Vecteur au lieu de MonVecteur.

Tiens, je viens de penser à une autre 3eme solution à ce problème. Remplacer
Vecteur dans son module par la nouvelle classe directement. Un truc du
genre :

import Vecteur
Vecteur.Vecteur = MonVecteur

Cela marche très bien sauf pour 1 cas : les modules qui font "from Vecteur
import Vecteur". Dans ce cas il faut bien s'arranger pour faire le
remplacement avec que ces module soit importé.




PS : moi j'ai pas de problème avec ça, je ne fais que de discuter pour
trouver la solution la plus élégante.



Avatar
Bruno Desthuilliers
bruno at modulix wrote:


Christophe wrote:




(snip)


BTW, j'en profite pour recommander la BonnePratique(tm) suivante : ne
pas coder le nom de la classe en dur dans le code. Christophe





s/Christophe/François/, bien sûr. Désolé :(

n'aurait
pas ce problème si scale() avait été implémenté ainsi:

def scale(self, scale):
return self.__class__(self.x*scale, self.y*scale, self.z*scale)



Facile à dire.



Facile à faire aussi dans *tes* développements !-)

Ce n'était pas une solution à ton problème, mais une pratique qui, si
elle avait été suivie par l'auteur de ta classe Vecteur, t'aurais évité
le problème que tu a maintenant à résoudre.



Oui mais c'est pas évident de penser à ça.

En conclusion quand même, on peut noter que ce genre de situation est moins
bloquante en Python que dans la pluspart des langages objets. Il y a
plusieurs solutions possibles qui ne nécessitent pas trop d'entretient
( modifier directement Vector, encapsuler Vector et faire un passthrough
avec __getattr__ et __setattr__. Il reste quand même le problème du code
externe qui peut retourner des nouveaux Vecteur au lieu de MonVecteur.

Tiens, je viens de penser à une autre 3eme solution à ce problème. Remplacer
Vecteur dans son module par la nouvelle classe directement. Un truc du
genre :

import Vecteur
Vecteur.Vecteur = MonVecteur

Cela marche très bien sauf pour 1 cas : les modules qui font "from Vecteur
import Vecteur". Dans ce cas il faut bien s'arranger pour faire le
remplacement avec que ces module soit importé.


Mmmm... Je n'aime pas trop ce genre de monkeypatch. Je sais bien que le
dynamisme de Python a peu de limites, mais quand j'appelle la classe
Vecteur, je m'attends quand même à avoir une instance de Vecteur, pas
une instance de MonVecteur !-)

(outre que dans ce cas, le comportement n'est pas très fiable)

Une autre encore serait de wrapper les méthode de Vecteur dans une
fonction basée sur la méthode _verifyVector() de ma solution précédente,
et d'affecter le résultat à la classe MyVector (ce qui peut être fait
directement dans le module au top-level ou dans une métaclasse).

PS : moi j'ai pas de problème avec ça,


Oui, mes excuses, j'ai corrigé plus haut.




Avatar
amaury
BTW, j'en profite pour recommander la BonnePratique(tm) suivante : ne
pas coder le nom de la classe en dur dans le code. Christophe n'aurait
pas ce problème si scale() avait été implémenté ainsi:

def scale(self, scale):
return self.__class__(self.x*scale, self.y*scale, self.z*scale)


Pas tout à fait d'accord: une classe dérivée peut avoir un autre
constructeur, qui n'est pas compatible, du genre NormalizedVector(rho,
phi).

--
Amaury

Avatar
bruno at modulix
amaury wrote:
BTW, j'en profite pour recommander la BonnePratique(tm) suivante : ne
pas coder le nom de la classe en dur dans le code. Christophe n'aurait
pas ce problème si scale() avait été implémenté ainsi:

def scale(self, scale):
return self.__class__(self.x*scale, self.y*scale, self.z*scale)



Pas tout à fait d'accord: une classe dérivée peut avoir un autre
constructeur, qui n'est pas compatible, du genre NormalizedVector(rho,
phi).

Effectivement. Mais dans ce cas, est-il judicieux d'utiliser l'héritage

? C'est une réutilisation partielle d'implémentation, sans sémantique de
sous-typage, donc la délégation serait peut-être plus appropriée ?

--
bruno desthuilliers
python -c "print '@'.join(['.'.join([w[::-1] for w in p.split('.')]) for
p in ''.split('@')])"


Avatar
Eric Brunel
On Tue, 29 Nov 2005 19:12:12 +0100, bruno at modulix wrote:

amaury wrote:
BTW, j'en profite pour recommander la BonnePratique(tm) suivante : ne
pas coder le nom de la classe en dur dans le code. Christophe n'aurait
pas ce problème si scale() avait été implémenté ainsi:

def scale(self, scale):
return self.__class__(self.x*scale, self.y*scale, self.z*scale)



Pas tout à fait d'accord: une classe dérivée peut avoir un autre
constructeur, qui n'est pas compatible, du genre NormalizedVector(rho,
phi).

Effectivement. Mais dans ce cas, est-il judicieux d'utiliser l'héritage

? C'est une réutilisation partielle d'implémentation, sans sémantique de
sous-typage, donc la délégation serait peut-être plus appropriée ?


Je n'ai pas l'impression que le fait d'avoir des constructeurs avec des signatures différentes veuille forcément dire qu'on n'a pas de sémantique de sous-typage. Si on prend l'exemple tarte à la crème de la classe Rectangle qui spécialise la classe Polygone, les paramètres du constructeur vont très certainement être différents: on sent bien une liste de points pour celui de Polygone, et des coordonnées + une largeur et une hauteur pour Rectangle. Ca paraît assez naturel de faire comme ça, mais ça rend l'utilisation de self.__class__ pour créer un objet impossible dans Polygone. Bien sûr, on pourrait accepter les paramètres de constructeur de Polygone dans Rectangle, vérifier qu'ils représentent bien un rectangle et lever une exception si ce n'est pas le cas, mais ça paraît bien lourdingue... Et pourtant, Rectangle est bien une spécialisation de Polygone (ou alors, c'est que tous les bouquins nous mentent depuis qu'on est tout petit...).

C'est vrai que d'avoir des signatures de méthodes qui changent entre une super-classe et une sous-classe est fréquemment le signe que la hiérarchie de classes est mal partie. Mais pour moi, les constructeurs ont toujours été plus ou moins exclus de cette règle: une sous-classe introduit en général des contraintes en plus, donc pouvoir construire un objet de la sous-classe avec les mêmes infos que les objets de la super-classe peut très bien ne pas être approprié. Donc le truc de créer des instances avec self.__class__ me paraît pour le moins risqué...

Mais bon, moi, ce que j'en dis...
--
python -c "print ''.join([chr(154 - ord(c)) for c in 'U(17zX(%,5.zmz5(17;8(%,5.Z65'*9--56l7+-'])"



Avatar
bruno at modulix
Eric Brunel wrote:
On Tue, 29 Nov 2005 19:12:12 +0100, bruno at modulix
wrote:

amaury wrote:

BTW, j'en profite pour recommander la BonnePratique(tm) suivante : ne
pas coder le nom de la classe en dur dans le code. Christophe n'aurait
pas ce problème si scale() avait été implémenté ainsi:

def scale(self, scale):
return self.__class__(self.x*scale, self.y*scale, self.z*scale)




Pas tout à fait d'accord: une classe dérivée peut avoir un autre
constructeur, qui n'est pas compatible, du genre NormalizedVector(rho,
phi).

Effectivement. Mais dans ce cas, est-il judicieux d'utiliser l'héritage

? C'est une réutilisation partielle d'implémentation, sans sémantique de
sous-typage, donc la délégation serait peut-être plus appropriée ?



Je n'ai pas l'impression que le fait d'avoir des constructeurs avec des
signatures différentes veuille forcément dire qu'on n'a pas de
sémantique de sous-typage.


Ca dépend de la définition de "sous-typage" !-)

Si on prend l'exemple tarte à la crème de la
classe Rectangle qui spécialise la classe Polygone,


Et vlan ! Non seulement c'est une tarte à la crème, mais c'est aussi une
grossière erreur de conception - la preuve :

les paramètres du
constructeur vont très certainement être différents: on sent bien une
liste de points pour celui de Polygone, et des coordonnées + une largeur
et une hauteur pour Rectangle.


Le fait que mathématiquement, un rectangle soit "une sorte de" polygone
n'implique pas que la classe (informatique) représentant un rectangle
soit une sous-classe (au sens de l'héritage d'implémentation) de la
classe Polygone. Au contraire, comme tu le démontre, ces deux classes
présentent des divergences manifestes. Il convient donc de déterminer ce
qu'elles ont de commun de ce qui les distingue fondamentalement.

Dans un langage comme Java ou C++, on utilisera le premier comme
'racine' (forcément abstraite) du type (classe abstraite, ou interface
en Java). En Python, le typage étant dynamique, on se contentera, s'il y
a des éléments communs d'implémentation, de les factoriser dans une
classe mixin (classe sans état).

Bref, tu te retrouverais avec:

class PolygonMixin(object):
# opérations de base communes à tous les polygones
(...)

class Polygone(Shape, PolygoneMixin):
def __init__(self, points):
(...)

class Rectangle(Shape, PolygonMixin):
def __init__(self, x, y, h, w):
(...)


BTW, il y avait un excellent article sur ce sujet, concernant les
rectangles et les carrés, mais je n'ai plus l'URL (si quelqu'un la
retrouve..).

L'idée principale était que, si on utilise soit les contrats (le
résultat serait sensiblement le même avec les test unitaires je pense),
on se rendait vite compte que même si un carré est un rectangle, Carré
ne pouvait pas être une sous-classe de Rectangle. En effet, modifier la
largeur d'un rectangle est supposé ne pas modifier sa hauteur, alors que
modifier la largeur d'un carré implique de modifier sa hauteur. En fait,
un carré n'a ni hauteur ni largeur, il a un côté.

Ca paraît assez naturel de faire comme
ça,


Du point de vue analyse préalable, oui, bien sûr, puisqu'à ce moment tu
modélise le domaine du problème, *dans ses propres termes*.

Du point de vue conception détaillée, ce serait une erreur.

mais ça rend l'utilisation de self.__class__ pour créer un objet
impossible dans Polygone. Bien sûr, on pourrait accepter les paramètres
de constructeur de Polygone dans Rectangle, vérifier qu'ils représentent
bien un rectangle et lever une exception si ce n'est pas le cas, mais ça
paraît bien lourdingue...


Lourdingue est un euphémisme. On touche aussi là une limitation du
typage dynamique à la Python : l'absence de multidispatch. Ta solution
serait envisageable s'il y avait un dispatch non seulement sur le type
du recepteur du message, mais aussi sur les types des arguments du
message. Ce serait même une solution parfaitement idiomatique en Java ou
en C++.

Et pourtant, Rectangle est bien une
spécialisation de Polygone


En math, oui. Pas en informatique.

(ou alors, c'est que tous les bouquins nous
mentent depuis qu'on est tout petit...).


Les bouquins de math, non. Quant aux bouquins d'informatique à deux
balles qui donnent ça comme exemple de cas d'utilisation de l'héritage,
la décence m'interdis d'expliquer ici comment en faire bon usage :(

C'est vrai que d'avoir des signatures de méthodes qui changent entre une
super-classe et une sous-classe est fréquemment le signe que la
hiérarchie de classes est mal partie.


Ca dépend si tu considères que classe == type ou que ces deux concepts
sont totalement disjoint (ou n'importe quelle position intermédiaire...)
- mais effectivement, ce peut être signe d'un problème de conception.

Dans les langages à typage statique (et plus généralement dans une
perspective historique), les deux concepts sont liés, et le même
mécanisme (l'héritage) est utilisé aussi bien pour le sous-typage que
pour la réutilisation d'implémentation. Dans un langage à typage
dynamique, le statut de l'héritage (est-ce un sous-typage ou juste une
réutilisation) est plus bancal, puisque l'héritage n'est pas nécessaire
au sous-typage, mais que *généralement* il l'induit.

La plupart des intros à l'OO mettent beaucoup trop en valeur l'héritage,
qui n'est à l'origine qu'une facilité, et n'est en rien nécessaire -
sauf bien sûr quand il sert également de support pour le polymorphisme.

Mais pour moi, les constructeurs
ont toujours été plus ou moins exclus de cette règle: une sous-classe
introduit en général des contraintes en plus,


Ce n'est un problème que si ces contraintes sont incompatibles avec
celles de la superclasse, et dans ce cas, c'est peut-être une indication
que le sous-classage (<> sous-typage en Python) n'est pas la solution
adaptée.

donc pouvoir construire un
objet de la sous-classe avec les mêmes infos que les objets de la
super-classe peut très bien ne pas être approprié.


Pour moi, je préfère que la signature de l'initialisateur d'une
sous-classe soit *compatible* avec celle de l'initialisateur de la
superclasse.

Donc le truc de créer
des instances avec self.__class__ me paraît pour le moins risqué...


C'est un choix de conception... Note que le problème peut aussi se poser
en dehors de tout lien d'héritage - quand la classe à instancier est
choisie dynamiquement par exemple (ex: système de plugin).

Dans la mesure où Python permet la réutilisation d'implémentation par
d'autres moyens que l'héritage, sans préjudice pour le "typage", et sans
imposer la réécriture manuelle de toutes les délégations, il me semble
préférable d'assurer la compatibilité maximum entre une classe et ses
sous-classes, et d'utiliser d'autres moyens (mixins, délégation) pour la
factorisation ou la simple réutilisation d'implémentation.

En bref, si ta sous-classe n'est pas compatible avec la classe de base,
ce n'est pas un sous-type de la classe de base (selon la définition de
Liskov, qui n'est bien sûr pas la seule possible ou valide), et donc il
vaut mieux utiliser dans ce cas un autre mécanisme que l'héritage.

Mais bon, moi, ce que j'en dis...


Pareil !-)

--
bruno desthuilliers
python -c "print '@'.join(['.'.join([w[::-1] for w in p.split('.')]) for
p in ''.split('@')])"




Avatar
Paul Gaborit
À (at) Wed, 30 Nov 2005 14:51:23 +0100,
bruno at modulix écrivait (wrote):
[...]
Et pourtant, Rectangle est bien une
spécialisation de Polygone


En math, oui. Pas en informatique.

(ou alors, c'est que tous les bouquins nous
mentent depuis qu'on est tout petit...).


Les bouquins de math, non. Quant aux bouquins d'informatique à deux
balles qui donnent ça comme exemple de cas d'utilisation de l'héritage,
la décence m'interdis d'expliquer ici comment en faire bon usage :(
[...]

La plupart des intros à l'OO mettent beaucoup trop en valeur l'héritage,
qui n'est à l'origine qu'une facilité, et n'est en rien nécessaire -
sauf bien sûr quand il sert également de support pour le polymorphisme.


C'est tout à fait vrai et je peste régulièrement contre ce genre de
bouquin.

Un autre mauvais exemple d'héritage pourtant souvent cité est celui de
l'héritage qu'il y aurait entre les classes "directeur", "commercial",
"opérateur", etc. et la classe de base "salarié". Cela semble
séduisant sur le papier... jusqu'au jour où on s'aperçoit qu'un
"commercial" peut devenir un "directeur" tout en restant "salarié". La
durée de vie de l'objet "commercial" ne correspond donc pas à celle de
l'objet "salarié". Ce n'est donc pas une situation d'héritage (au sens
informatique) mais il est parfois trop tard pour corriger...

De manière générale, la relation "est un" entre deux objets est
nécessaire mais *non* *suffisante* pour caractériser une relation
d'héritage. La relation d'héritage est en fait très rare.

--
Paul Gaborit - <http://perso.enstimac.fr/~gaborit/>


Avatar
Eric Brunel
On Wed, 30 Nov 2005 14:51:23 +0100, bruno at modulix wrote:
Eric Brunel wrote:
Je n'ai pas l'impression que le fait d'avoir des constructeurs avec des
signatures différentes veuille forcément dire qu'on n'a pas de
sémantique de sous-typage.


Ca dépend de la définition de "sous-typage" !-)

Si on prend l'exemple tarte à la crème de la
classe Rectangle qui spécialise la classe Polygone,


Et vlan ! Non seulement c'est une tarte à la crème, mais c'est aussi une
grossière erreur de conception - la preuve :

les paramètres du
constructeur vont très certainement être différents: on sent bien une
liste de points pour celui de Polygone, et des coordonnées + une largeur
et une hauteur pour Rectangle.


Le fait que mathématiquement, un rectangle soit "une sorte de" polygone
n'implique pas que la classe (informatique) représentant un rectangle
soit une sous-classe (au sens de l'héritage d'implémentation) de la
classe Polygone. Au contraire, comme tu le démontre, ces deux classes
présentent des divergences manifestes. Il convient donc de déterminer ce
qu'elles ont de commun de ce qui les distingue fondamentalement.


Après 4 ou 5 relectures et une bonne nuit de sommeil, je sens que j'approche de l'illumination...

[snip]
BTW, il y avait un excellent article sur ce sujet, concernant les
rectangles et les carrés, mais je n'ai plus l'URL (si quelqu'un la
retrouve..).

L'idée principale était que, si on utilise soit les contrats (le
résultat serait sensiblement le même avec les test unitaires je pense),
on se rendait vite compte que même si un carré est un rectangle, Carré
ne pouvait pas être une sous-classe de Rectangle. En effet, modifier la
largeur d'un rectangle est supposé ne pas modifier sa hauteur, alors que
modifier la largeur d'un carré implique de modifier sa hauteur. En fait,
un carré n'a ni hauteur ni largeur, il a un côté.


... ce qui ne pose aucun problème en maths, vu que les objets y sont immuables. Donc même si on a une opération qui permet de modifier le côté du rectangle, elle ne modifiera pas un rectangle existant, mais en construira un nouveau. Appliquée à un carré, elle construira donc un rectangle, ce qui reste correct.

On a l'air de retomber sur le problème classique des effets de bord. Ne serait-ce pas (encore une fois...) de là que viendrait cette distinction entre le concept informatique de sous-typage et l'inclusion mathématique d'un ensemble d'instances dans un autre? Parce qu'a priori, les opérations ne modifiant pas l'objet ont l'air de ne pas poser de problème: tout ce qu'on peut demander à un polygone, on peut le demander à un rectangle aussi, non?

[snip]
(ou alors, c'est que tous les bouquins nous
mentent depuis qu'on est tout petit...).


Les bouquins de math, non. Quant aux bouquins d'informatique à deux
balles qui donnent ça comme exemple de cas d'utilisation de l'héritage,
la décence m'interdis d'expliquer ici comment en faire bon usage :(


(Voilà qui m'a fait doucement rigoler, puisque l'un de ces bouquins est celui de B. Meyer, créateur d'Eiffel et grand défenseur de la conception par contrat; il utilise la hiérarchie Polygone -> Rectangle -> Carré comme exemple dans tout le bouquin, alors que précisément, une post-condition sur une opération de modification d'un rectangle peut très bien être violée pour un carré. Livre à n'utiliser qu'en suppositoire, donc, au moins pour cette partie-là...)

[snip]
En bref, si ta sous-classe n'est pas compatible avec la classe de base,
ce n'est pas un sous-type de la classe de base (selon la définition de
Liskov, qui n'est bien sûr pas la seule possible ou valide), et donc il
vaut mieux utiliser dans ce cas un autre mécanisme que l'héritage.


(Pour ceux que ça intéresse: http://en.wikipedia.org/wiki/Liskov_substitution_principle (pas traduit en français malheureusement...))
--
python -c "print ''.join([chr(154 - ord(c)) for c in 'U(17zX(%,5.zmz5(17;8(%,5.Z65'*9--56l7+-'])"


Avatar
Do Re Mi chel La Si Do
Bonjour !

de retomber sur le problème classique des effets de bord.




D'un certain point de vue, oui. Comme la normalisation/dénormalisation des
bases de données. Comme la taxinomie. Comme ma quadrature du cercle (en
math). Comme l'élaboration d'une convention collective. Etc.

On tourne toujours autour des mêmes limites du raisonnement. Et, d'une
certaine manière, cela à été démontré par Gödel, avec son théorème sur
l'incomplétude des systèmes.

Conclusion 1 : inutile de se droguer, pour planer un peu : il suffit de
faire de l'informatique.
Conclusion 2 : pour frimer, pas la peine de se pavaner en AirNess, un peu de
Python suffit.

@-salutations

Michel Claveau



1 2 3 4