Plan du site  
français  English
pixel
pixel

Articles - Étudiants SUPINFO

Héritage simple

Après avoir rappelé la définition d'une relation d'héritage entre classes, on va voir comment l'implémenter en Python.

Rappel du concept

L'héritage est une relation de spécialisation/généralisation entre deux classes. Elle indique qu’une classe dite classe fille spécialise une autre classe dite classe mère, i.e. qu’elle possède les attributs et les méthodes de la classe mère plus d’autres qui lui sont propres. On parle aussi de super classe et de sous classe.

Syntaxe générale

Voici comment on implémente une relation d'héritage en Python :

class classeMere:
    """une classe mère"""
    ...

class classeFille(classeMere):
    """une classe fille héritant de la classe mère"""
    ...

Example 1.1. Un héritage entre une classe "rectangle" et une classe "pavé droit"

Un pavé droit pouvant être vu comme un rectangle à trois dimensions, i.e. un rectangle bien spécial, la relation d'héritage suivante a bien du sens :

class rectangle:
    """gestion de rectangles"""
    ...

class paveDroit(rectangle):
    """gestion de boîtes à chaussures"""
    ...

La visibilité protégée

Pour l'instant nous avons rencontré deux types de visibilité pour les attributs :

  • publique : les attributs sont accessibles de partout.

  • privée : les attributs ne sont accessibles qu’au sein de la classe.

Il est facile de comprendre que cela est insuffisant pour respecter au mieux une relation d'héritage. On aimerait en effet avoir la possibilité pour un attribut d'être manipulable par les classes filles de sa propre classe. Nous allons donc avoir besoin d'une troisième sorte de visibilité.

Cette remarque conduit à la définition suivante : un attribut est dit protégé si son utilisation est limitée à la classe et ses descendantes.

[Note]

Il en est de même pour les méthodes.

Pour déclarer un attribut de façon protégée, lors de l’implémentation de la méthode __init__ on fera précéder son nom par un simple underscore :

def __init__(self,para1,para2,...):
    attributPublic = valeur
    _attributProtege = valeur
    __attributPrive = valeur
    ...
[Note]

D'autres langages seront plus explicites, avec l'utilisation d'un mot clé comme "protected".

Example 1.2. Une classe "rectangle"

Les attributs de notre classe mère "rectangle" vont être déclarés protégés :

class rectangle:
    def __init__(self,x,y):
        self._x = x
        self._y = y
    def surface(self):
        return self._x*self._y

[Note]

En Python, la déclaration d’un attribut avec une visibilité protégée n’est qu’une indication à l’égard des utilisateurs de la classe. En effet on pourra quand même accéder à l’attribut en question depuis l’extérieur de la classe. Les langages C++ et Java seront plus stricts à ce sujet. Ceci dit, même s’il ne s’agit que d’une indication, on la suivra à la lettre.

Example 1.3. Une mauvaise pratique

Une utilisation qui viole le principe d'encapsulation :

photo = rectangle(10,15)
print(photo._x)
print(photo._y)
10
15

On peut quand même accéder à « _x » et « _y » depuis l’extérieur de la classe. Mais on ne le fera pas (plus).


Le constructeur de la classe fille

On devra respecter une règle fondamentale : le constructeur de la classe fille doit faire un appel explicite au constructeur de la classe mère afin d’initialiser les attributs hérités de celle-ci. Pour cela on aura deux syntaxes à notre disposition.

Dans la première syntaxe possible, on fait précéder __init__ du nom de la classe mère :

class classeFille(classeMere):
    """documentation de la classe fille"""
    def __init__(self,para1,para2,...):
        classeMere.__init__(self,para1,...)
        ...

Example 1.4. Une classe "pavé droit" héritant de la classe "rectangle", première syntaxe

Le constructeur de la classe "pavé droit" appelle celui de sa classe mère "rectangle" avec la syntaxe rectangle.__init__(...) :

class paveDroit(rectangle):
    def __init__(self,x,y,z):
        rectangle.__init__(self,x,y)
        self.__z = z
    def volume(self):
        return self._x*self._y*self.__z

photo = rectangle(3,4)
print(photo.surface())
weston = paveDroit(3,4,10)
print(weston.volume())
12
120

Dans la seconde syntaxe autorisée, on fait précéder __init__ du mot clé super() :

class classeFille(classeMere):
    """documentation de la classe fille"""
    def __init__(self,para1,para2,...):
        super().__init__(para1,...)
        ...

Example 1.5. Une classe "pavé droit" héritant de la classe "rectangle", seconde syntaxe

Le constructeur de la classe "pavé droit" appelle celui de sa classe mère "rectangle" avec la syntaxe super().__init__(...) :

class paveDroit(rectangle):
    def __init__(self,x,y,z):
        super().__init__(x,y)
        self.__z = z
    def volume(self):
        return self._x*self._y*self.__z

Transmission des membres

On va ici revenir via des exemples sur la transmission des membres (attributs et méthodes) selon leur visibilité. Cela permettra de fixer définitivement les idées.

Puisqu'il est bon de se répéter, revenons une énième fois sur les définitions de visibilité :

  • publique : les membres sont accessibles de partout.

  • protégé : les membres ne sont accessible qu’au sein de la classe et de ses descendantes.

  • privée : les attributs ne sont accessibles qu’au sein de la classe.

Example 1.6. Un membre public

Ici on utilise sans soucis une méthode publique de la classe mère dans la classe fille. Pour cela on fait précéder le nom de la méthode par le mot clé super() :

class paveDroit(rectangle):
    def __init__(self,x,y,z):
        super().__init__(x,y)
        self.__z = z
    def volume(self):
        return super().surface()*self.__z

Même chose avec une autre syntaxe, où cette fois ci le nom de la méthode est précédée par le mot clé self :

class paveDroit(rectangle):
    def __init__(self,x,y,z):
        super().__init__(x,y)
        self.__z = z
    def volume(self):
        return self.surface()*self.__z

Example 1.7. Des membres protégés

Ici on utilise sans problèmes des attributs protégés de la classe mère dans la classe fille :

class paveDroit(rectangle):
    def __init__(self,x,y,z):
        rectangle.__init__(self,x,y)
        self.__z = z
    def volume(self):
        return self._x*self._y*self.__z

Example 1.8. Des membres privés

Ici on constate l'utilisation impossible d’attributs privés de la classe mère dans la classe fille :

class rectangle:
    def __init__(self,x,y):
        self.__x = x
        self.__y = y
    def surface(self):
        return self.__x*self.__y  

class paveDroit(rectangle):
    def __init__(self,x,y,z):
        rectangle.__init__(self,x,y)
        self.__z = z
    def volume(self):
        return self.__x*self.__y*self.__z

santoni = paveDroit(2,3,4)
print(santoni.volume())
AttributeError: 'paveDroit' object has no attribute '_paveDroit__x'

Héritage multiple

Comme dans la partie précédente, après un rappel de la définition on étudiera son implémentation en Python.

Rappel du concept

L'héritage multiple est la possibilité pour une classe de posséder plusieurs classes mères.

[Note]

Comme pour un héritage simple, la classe fille possède les attributs et les méthodes de ses classes mères plus d’autres qui lui sont propres.

Syntaxe générale

Voici comment on implémente une relation d'héritage multiple en Python :

class mere1:
    """une première classe mère"""
    ...

class mere2:
    """une seconde classe mère"""
    ...

class classeFille(mere1,mere2,...):
    """une classe fille héritant des classes mère1 et mère2"""
    ...
[Note]

On verra dans la suite que l'ordre dans lequel on énumère les classes mères lors de la déclaration de la classe fille a de l'importance.

Example 1.9. Une classe "omnivore" qui hérite des classes "herbivore" et "carnivore"

Un omnivore étant à la fois un carnivore et un herbivore, la relation d'héritage multiple suivante a bien du sens :

class carnivore:
    """gestions des carnivores"""
    ...

class herbivore:
    """gestion des herbivores"""
    ...

class omnivore(carnivore,herbivore):
    """gestion des omnivores"""
    ...

Le constructeur de la classe fille

On devra respecter une règle fondamentale : le constructeur de la classe fille doit faire un appel explicite au constructeur des classes mères afin d’initialiser les attributs hérités de celles-ci :

class classeFille(mere1,mere2,...):
    """documentation de la classe fille"""
    def __init__(self,para1,para2,...):
        mere1.__init__(self,para1,...)
        mere2.__init__(self,para1,...)
        ...
[Note]

Il est évident que contrairement au cas d'un héritage simple, on ne peut pas utiliser une syntaxe reposant sur le mot clé super(). On ne saurait pas en effet à laquelle des classes mères on fait référence.

Example 1.10. Le constructeur de la classe "omnivore"

Le constructeur de la classe "omnivore" appelle ceux de ses classes mères "carnivore" et "herbivore" avec la syntaxe carnivore.__init__(...) et herbivore.__init__(...) :

class carnivore:
    def __init__(self,p):
        self._poidsViande = p
    def devorer(self):
        print("Je mange",self._poidsViande,"kilogs de steack par jour")

class herbivore:
    def __init__(self,p):
        self._poidsHerbe = p
    def brouter(self):
        print("Je mange",self._poidsHerbe,"kilogs de gazon par jour")

class omnivore(carnivore,herbivore):
    def __init__(self,pv,ph,h):
        carnivore.__init__(self,pv)
        herbivore.__init__(self,ph)
        self.__humain = h

teddy = omnivore(10,5,False)
teddy.devorer()
teddy.brouter()
Je mange 10 kilogs de steack par jour
Je mange 5 kilogs de gazon par jour

Recherche dans une hiérarchie

La question est ici de savoir comment et dans quel ordre s'effectue la recherche d'une méthode n'appartenant pas à une classe mais éventuellement à une de ses classes mères.

On considère donc une relation d'héritage multiple de la forme :

class fille(mere1,mere2,mere3...):
    """une classe fille héritant des classes mère1,mère2,mère3,..."""
    ...

Si sur un objet de la classe fille on appelle une méthode qui n’appartient pas à cette classe, la recherche s’effectuera de gauche à droite dans la liste des classes mères.

Cela signifiera que l’on regardera d’abord si la classe "mère1" possède la méthode en question :

  • Si oui, on applique cette méthode et la recherche est terminée.

  • Si non, on considère la classe “mère2“. Et ainsi de suite en prenant les classes de gauche à droite.

[Note]

Si la recherche est infructueuse dans les classes "mère1", "mère2", ... on la continue dans les classes parentes de celles-ci.

On procède alors de la même façon que précédemment, en parcourant la liste de la gauche vers la droite.

Example 1.11. Une recherche dans une hiérarchie de classes

Voici nos deux classes mères :

class a:
    """une classe a"""
    def exemple(self):
        print("je l'ai trouvée dans a")

class b:
    """une classe b"""
    def exemple(self):
        print("je l'ai trouvée dans b")

On définit une troisième classe "c" qui hérite d'abord de "a" puis de "b" :

class c(a,b):
    """une classe c qui hérite de a et b"""

test = c()
test.exemple()
je l'ai trouvée dans a

On fait maintenant hériter "c" d'abord de "b" puis de "a" :

class c(b,a):
    """une classe c qui hérite de a et b"""

test = c()
test.exemple()
je l'ai trouvée dans b

[Note]

Cet exemple est purement pédagogique, n'y cherchez pas de sens profond...

Héritage versus composition

On va présenter maintenant un concept pouvant être une alternative à l'héritage, et on effectuera la comparaison entre eux.

La relation de composition

La relation de composition modélise une relation d’inclusion entre les instances de deux classes. Les objets de la classe conteneur possèdent donc un attribut qui est un objet de la classe contenue.

[Note]

Cette relation sera détaillée dans le cours d'UML, où l'on verra également une notion très proche : l'agrégation.

On peut utiliser une relation de composition entre classes si on peut établir un lien sémantique du type “possède” ou “a un”.

Example 1.12. Des relations de compositions

  • Une voiture a une plaque d’immatriculation.

  • Un livre possède des pages.


Un exemple simple

On va implémenter ce concept sur un exemple simple. On va considérer une classe “point” possédant deux attributs : abcisse et ordonnée. On va ensuite déclarer une classe disque, qui possèdera également deux attributs : rayon (un nombre réel) et centre qui sera un objet de la classe "point". Cela a bien du sens car un disque possède un centre.

Example 1.13. Des classes "point" et "disque"

La classe "disque" possède un attribut qui est un objet de la classe "point" :

class point:
    def __init__(self,x,y):
        self.__x = x
        self.__y = y
    def getx(self):
        return self.__x
    def gety(self):
        return self.__y

class disque:
    def __init__(self,x,y,r):
        self.__r = r
        self.__centre = point(x,y)
    def surface(self):
        return 3.14*self.__r**2
    def getCentre(self):
        return self.__centre

cd = disque(-1,2,5)
print("abscisse du centre :",cd.getCentre().getx())
print("ordonnée du centre :",cd.getCentre().gety())
abscisse du centre : -1
ordonnée du centre : 2

Une alternative à l'héritage

Dans certains cas il est tout à fait possible techniquement d’utiliser une relation de composition à la place d’une relation d’héritage.

Au lieu d’avoir une classe B qui hérite d’une classe A, on déclare dans B un attribut qui sera une instance de la classe A.

Example 1.14. Retour sur les classes "rectangle" et "pavé droit"

Reprenons notre classe "rectangle", mais cette fois-ci nous n'allons plus implémenter de relation d'héritage mais une relation de composition. Nos attributs seront donc déclarés privés et non protégés comme précédemment :

class rectangle:
    def __init__(self,x,y):
        self.__x = x
        self.__y = y
    def surface(self):
        return self.__x*self.__y
    def getx(self):
        return self.__x
    def gety(self):
        return self.__y

Avec cette approche, la classe "pavé droit" possèdera un attribut de la classe "rectangle" :

class paveDroitBis:
    def __init__(self,x,y,z):
        self.base = rectangle(x,y)
        self.__z = z
    def volume(self):
        return self.base.getx()*self.base.gety()*self.__z

A noter que l'on aurait pu aussi utiliser la méthode "surface" pour implémenter la méthode "volume". Procéder ainsi donne un exemple de manipulation d’accesseurs et de comment on doit les chaîner.

weston = paveDroitBis(2,3,5)
print(weston.base.surface())
print(weston.volume())
6
30

Que choisir ?

On choisit l’héritage quand la relation entre classes est bien de la forme “est un”, ou pour les anglicistes “is a”.

On choisit la composition quand la relation entre classes est bien de la forme “a un”, ou pour les anglicistes “has a”.

Example 1.15. Retour sur certains exemples précédents du point de vue composition vs héritage

  • Dans le cas du disque et du centre, implémenter une relation d'héritage aurait été possible mais n'aurait guère eu de sens, car il est clair qu'un disque a un centre.

  • Par contre, dans la relation entre omnivore et herbivore le choix logique était bien celui de l'héritage car un omnivore est bien un herbivore particulier.

  • Pour ce qui est du rectangle et du pavé droit les choses sont moins évidentes. On peut bien sûr dire qu'un pavé droit est un rectangle à 3 trois dimensions. Mais également qu'un pavé droit a un rectangle comme base. Dans ce genre de cas les deux concepts peuvent s'appliquer...


[Note]

On peut aussi préférer une relation de composition afin de respecter stricto sensu le principe d’encapsulation. En effet une relation d'héritage s'accompagne la plupart du temps d'attributs déclarés protégés et donc accessibles dans des classes qui ne sont pas les leurs (même s'il s'agit de classes filles).

[Note]

La relation de composition est également plus simple à maintenir en cas de modifications du code.

A propos de SUPINFO | Contacts & adresses | Enseigner à SUPINFO | Presse | Conditions d'utilisation & Copyright | Respect de la vie privée | Investir
Logo de la société Cisco, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société IBM, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Sun-Oracle, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Apple, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Sybase, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Novell, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Intel, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Accenture, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société SAP, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Prometric, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo du IT Academy Program par Microsoft, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management

SUPINFO International University
Ecole d'Informatique - IT School
École Supérieure d'Informatique de Paris, leader en France
La Grande Ecole de l'informatique, du numérique et du management
Fondée en 1965, reconnue par l'État. Titre Bac+5 certifié au niveau I.
SUPINFO International University is globally operated by EDUCINVEST Belgium - Avenue Louise, 534 - 1050 Brussels