Menu du jour¶

  • plat principal: l'héritage
  • dessert: notions complémentaires (property, getter, setter, méthodes statiques)

Héritage¶

La relation Être¶

Héritage = relation entre classes de la forme

  • un objet B est un objet A particulier.
  • A est plus générique, B est plus spécifique.
  • conséquence: naturellement, on a en général plusieurs classes B, C, D qui héritent de A, qui spécialisent A.
  • vocabulaire: quand B, C, D héritent de A, ce sont des classes filles ou sous-classes de A, A est leur classe mère.
  • symbole UML: flèche vide
classDiagram
    A <|-- B
    A <|-- C
    A <|-- D
  • Un LivretA est un Compte. Un CompteCourant est un Compte.
  • Ce sont des comptes particuliers. Un Compte n'est pas forcément un LivretA.
classDiagram
    Compte <|-- LivretA
    Compte <|-- CompteCourant
    class Compte{
        titulaire : Personne
        solde : float
        Compte(titulaire)
        retire(montant) float
        dépose(montant) float
    }

    class LivretA{
        taux : float
        plafond : float
    }

    class CompteCourant{
        découvert_autorisé : float
    }
  • Un Étudiant est une Personne. Un EnseignantChercheur est une Personne.
  • Ce sont des personnes particulières. Une Personne n'est pas forcément un Étudiant.
classDiagram
    Personne <|-- Étudiant
    Personne <|-- EnseignantChercheur
    class Personne{
        nom : str
        prénom : str
        date_naissance : Date
        Personne(nom, prénom, date_naissance)
        calcule_age() int
        get_email() str
    }

    class EnseignantChercheur{
        EnseignantChercheur(nom, prénom, date_naissance, laboratoire)
        laboratoire : str
    }

    class Étudiant{
        Étudiant(nom, prénom, date_naissance, numéro_étudiant)
        numéro_étudiant  : int
    }
from datetime import date

prof = EnseignantChercheur("Turing", "Alan", date(1936, 6, 23), "Maths Cambridge")
print(prof.calcule_age())
classDiagram
    Personnage <|-- Magicien
    Personnage <|-- Guerrière
    Personnage <|-- Gobelin
    class Personnage{
        points_vie
        attaque
    }

    class Guerrière{
        defense
    }

    class Gobelin{
    }

    class Magicien{
        attaque_magique
    }
  • Un Triangle est un Polygone, un Carré est un Polygone.
  • Ce sont des polygones particuliers. Un Polygone n'est pas forcément un Triangle.
classDiagram
    Polygone <|-- Carré
    Polygone <|-- Triangle
    class Polygone{
        sommets : tuple de Point
        Polygone(liste_sommets)
        périmètre() float
        aire() float
    }
    class Carré{
        sommets : tuple de 4 Point
        Carré(sommet1, sommet2, sommet3, sommet4)
        périmètre() float
        aire() float
    }
    class Triangle{
        sommets : tuple de 3 Point
        Triangle(sommet1, sommet2, sommet3)
        périmètre() float
        aire() float
    }

Hiérarchie des classes:

  • héritage successifs : un Carré est un Rectangle qui est Quadrilatère qui est un Polygone, donc un Carré est un Polygone.
  • héritage multiple : un Carré est un Rectangle spécifique et un Losange spécifique.
classDiagram
    Polygone <|-- Quadrilatère
    Quadrilatère <|-- Losange
    Quadrilatère <|-- Rectangle
    Rectangle <|-- Carré
    Losange <|-- Carré
    Polygone <|-- Triangle
    Polygone <|--Pentagone

A est plus générique, B, C et D sont plus spécifiques:

  • ce qui est commun à tous les A est défini dans A et automatiquement hérité dans les classes filles B, C, D
  • chaque classe fille définit ses spécificités telles que:
    • nouveaux attributs
    • nouvelles méthodes
    • comportement différent d'une méthode existant dans la classe mère
    • etc.
classDiagram
    A <|-- B
    A <|-- C
    A <|-- D

C'est super: syntaxe et fonctionnement¶

In [213]:
from  datetime import date


class Personne:
    def __init__(self, nom, prénom, date_naissance):
        self.nom = nom
        self.prénom = prénom
        self.date_naissance = date_naissance

    def calcule_age(self):
        return int((date.today() - self.date_naissance).days / 365)
In [214]:
class Étudiant(Personne):
    def __init__(self, nom, prénom, date_naissance, numéro_étudiant):
        super().__init__(nom, prénom, date_naissance)
        self.numéro_étudiant = numéro_étudiant

class EnseignantChercheur(Personne):
    def __init__(self, nom, prénom, date_naissance, laboratoire):
        super().__init__(nom, prénom, date_naissance)
        self.laboratoire = laboratoire
In [215]:
prof = EnseignantChercheur("Turing", "Alan", date(1936, 6, 23), "Maths Cambridge")
print(prof.nom)
print(prof.calcule_age())
Turing
88
In [216]:
class Personne:
    def __init__(self, nom, prénom, date_naissance):
        self.nom = nom
        self.prénom = prénom
        self.date_naissance = date_naissance

class Étudiant(Personne):
    def __init__(self, nom, prénom, date_naissance, numéro_étudiant):
        super().__init__(nom, prénom, date_naissance)
        self.numéro_étudiant = numéro_étudiant

class EnseignantChercheur(Personne):
    def __init__(self, nom, prénom, date_naissance, laboratoire):
        super().__init__(nom, prénom, date_naissance)
        self.laboratoire = laboratoire
  • class Étudiant(Personne) définit la relation d'héritage depuis la classe fille
  • constructeur:
    • super().__init__(...) appelle le constructeur de la classe mère
    • le constructeur de la classe mère définit les attributs génériques nom, prénom, date_naissance
    • on complète ensuite la construction par les spécificités de la classe fille
    • remarque: pas self dans super().__init__(...); voir le tuto sur super.
In [217]:
class Personne:
    def __init__(self, nom, prénom, date_naissance):
        self.nom = nom
        self.prénom = prénom
        self.date_naissance = date_naissance

    def calcule_age(self):
        return int((date.today() - self.date_naissance).days / 365)
        
class EnseignantChercheur(Personne):
    def __init__(self, nom, prénom, date_naissance, laboratoire):
        super().__init__(nom, prénom, date_naissance)
        self.laboratoire = laboratoire

prof = EnseignantChercheur("Turing", "Alan", date(1936, 6, 23), "Maths Cambridge")
print(prof.calcule_age())
88

Appel à la méthode calcule_age: si elle n'est pas définie dans EnseignantChercheur, la définition est héritée de la classe mère et automatiquement utilisée. Utilisez le debugger pour vous en rendre compte.

In [218]:
class Personne:
    def __init__(self, nom, prénom, date_naissance):
        self.nom = nom
        self.prénom = prénom
        self.date_naissance = date_naissance

    def __str__(self):
        return self.prénom + " " + self.nom
        
class EnseignantChercheur(Personne):
    def __init__(self, nom, prénom, date_naissance, laboratoire):
        super().__init__(nom, prénom, date_naissance)
        self.laboratoire = laboratoire

    def __str__(self):
        s = super().__str__()
        s += ' (' + self.laboratoire + ')'
        return s

prof = EnseignantChercheur("Turing", "Alan", date(1936, 6, 23), "Maths Cambridge")
print(prof)
Alan Turing (Maths Cambridge)

***Surcharge* d'une méthode (__str__):**

  • si une méthode est redéfinie dans la classe fille, elle est appelée en priorité
  • la redéfinition peut éventuellement se servir de la méthode de la classe mère via super()

Hiérarchie des classes en Python:

  • la classe object est la mère à la racine de toutes les classes, cela permet de définir des comportement par défaut de tous les objets, par exemple __str__
  • de mère en fille, les espaces de nom sont imbriqués, permettant un appel automatique à des méthodes ou attributs définis dans une classe mère
  • on peut connaitre la hiérarchie au-dessus d'une classe avec la méthode mro()
classDiagram
    object <|-- Polygone
    object <|-- Personne
    Personne <|-- EnseignantChercheur
    Personne <|-- Étudiant
    Polygone <|-- Quadrilatère
    Quadrilatère <|-- Losange
    Quadrilatère <|-- Rectangle
    Polygone <|-- Triangle
    Polygone <|--Pentagone
In [219]:
print(EnseignantChercheur.mro())
[<class '__main__.EnseignantChercheur'>, <class '__main__.Personne'>, <class 'object'>]

Exemple complet 1: étudiants et enseignants-chercheurs¶

In [220]:
class Personne:
    domaine = 'univ-amu.fr'
    
    def __init__(self, nom, prénom, date_naissance):
        self.nom = nom
        self.prénom = prénom
        self.date_naissance = date_naissance

    def calcule_age(self):
        return int((date.today() - self.date_naissance).days / 365)
        
    def email(self):
        return f'{self.prénom}.{self.nom}@{self.domaine}'.lower()

    def __str__(self):
        return f"{self.prénom} {self.nom} <{self.email()}>"
In [221]:
class EnseignantChercheur(Personne):
    def __init__(self, nom, prénom, date_naissance, laboratoire):
        super().__init__(nom, prénom, date_naissance)
        self.laboratoire = laboratoire

    def __str__(self):
        return f'{super().__str__()} ({self.laboratoire})'

class Étudiant(Personne):
    domaine = 'etu.univ-amu.fr'
    
    def __init__(self, nom, prénom, date_naissance, numéro_étudiant):
        super().__init__(nom, prénom, date_naissance)
        self.numéro_étudiant = numéro_étudiant

    def __str__(self):
        return f'{super().__str__()} ({self.numéro_étudiant})'
In [222]:
prof = EnseignantChercheur("Turing", "Alan", date(1936, 6, 23), "Maths Cambridge")
etu = Étudiant("Simpson", "Bart", date(2015, 3, 1), "123456")
print(prof)
print(etu)
Alan Turing <alan.turing@univ-amu.fr> (Maths Cambridge)
Bart Simpson <bart.simpson@etu.univ-amu.fr> (123456)

Observations:

  • le nom de domaine est un attribut de classe, commun à toutes les instances, surchagé dans Étudiant
  • la méthode email() est identique pour tous, donc définie dans Personne
  • elle fait appel à l'attribut domaine qui dépend de la sous-classe

Exemple 2: points, polygones, triangles, etc.¶

Attention, Polygone est composé de sommets Point, il n'y a pas de relation d'héritage: un point n'est pas un polygone, un polygone n'est pas un point

classDiagram
    Polygone <|-- Carré
    Polygone <|-- Triangle
    Polygone *-- Point
    class Polygone{
        sommets : tuple de Point
        Polygone(sommets)
        périmètre() float
        aire() float
    }
    class Carré{
        sommets : tuple de 4 Point
        Carré(sommet1, sommet2, sommet3, sommet4)
        périmètre() float
        aire() float
    }
    class Triangle{
        sommets : tuple de 3 Point
        Triangle(sommet1, sommet2, sommet3)
        périmètre() float
        aire() float
        est_rectangle() bool
        est_isocèle() bool
        est_équilatéral() bool
        cercle_inscrit() (Point, float)
        cercle_circonscrit() (Point, float)
    }

    class Point{
        x : float
        y : float
        Point(x, y)
        distance(other) float
    }
In [223]:
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def distance(self, other):
        return ((other.x - self.x) ** 2 + (other.y - self.y) ** 2) ** 0.5
In [224]:
class Polygone:
    def __init__(self, sommets):
        self.sommets = tuple(sommets)

    def périmètre(self):
        d = 0
        pivot = self.sommets[-1]
        for point in self.sommets:
            d += pivot.distance(point)
            pivot = point
        return d
In [225]:
class Triangle(Polygone):
    def __init__(self, sommet1, sommet2, sommet3):
        super().__init__([sommet1, sommet2, sommet3])

    def cercle_circonscrit(self):
        xc = None  # à compléter
        yc = None  # à compléter
        r = None # à compléter
        c = Point(xc, yc)
        return c, r
In [226]:
triangle = Triangle(Point(0, 0), Point(1, 1), Point(2, 0))
print(triangle.périmètre())
4.82842712474619

Exemple 3: donjons et dragons¶

Une classe mère pour l'ensemble des personnages, qui ont chacun

  • 2 attributs: des points de vie, un score d'attage
  • la possibilité d'attaquer un autre personnage
  • et de subir une attaque portée par un autre personnage
In [227]:
class Personnage:
    def __init__(self, vie, attaque):
        self.vie = vie
        self.attaque = attaque

    def se_faire_taper(self, personnage):
        self.vie -= personnage.attaque

    def taper(self, personnage):
        personnage.se_faire_taper(self)

Une classe fille Guerrière héritant de Personnage:

  • un attribut supplémentaire blocage (probabilité entre 0 et 1)
  • une Guerrière peut bloquer une attaque: surcharge de se_faire_taper
In [228]:
import random

class Guerrière(Personnage):
    def __init__(self, vie, attaque, blocage):
        super().__init__(vie, attaque)
        self.blocage = blocage

    def se_faire_taper(self, personnage):
        if self.blocage < random.rand():
            super().se_faire_taper(personnage)
In [ ]:
 

Une classe fille Magicien héritant de Personnage:

  • un attribut supplémentaire attaque_magique
  • un Magicien peut lancer des sorts: méthode supplémentaire lancer_sort
In [229]:
class Magicien(Personnage):
    def __init__(self, vie, attaque, attaque_magique):
        super().__init__(vie, attaque)
        self.attaque_magique = attaque_magique

    def lancer_sort(self, personnage):
        personnage.vie -= self.attaque_magique

La suite: faites jouer ensemble les différents personnages en TP!

Pour aller plus loin: héritage multiple¶

Exemple: un Doctorant est un EnseignantChercheur et un Étudiant.

classDiagram
    Personne <|-- Étudiant
    Personne <|-- EnseignantChercheur
    Étudiant <|-- Doctorant
    EnseignantChercheur <|-- Doctorant
    
    class Personne{
        nom : str
        prénom : str
        date_naissance : Date
        Personne(nom, prénom, date_naissance)
        calcule_age() int
        get_email() str
    }

    class EnseignantChercheur{
        EnseignantChercheur(nom, prénom, date_naissance, laboratoire)
        laboratoire : str
    }

    class Étudiant{
        Étudiant(nom, prénom, date_naissance, numéro_étudiant)
        numéro_étudiant  : int
    }

    class Doctorant{
        Doctorant(nom, prénom, date_naissance, laboratoire, numéro_étudiant)
    }
In [230]:
class Doctorant(Étudiant, EnseignantChercheur):
    def __init__(self, nom, prénom, date_naissance, numéro_étudiant, laboratoire):
        pass # Comment compléter?

print(Doctorant.mro())
[<class '__main__.Doctorant'>, <class '__main__.Étudiant'>, <class '__main__.EnseignantChercheur'>, <class '__main__.Personne'>, <class 'object'>]

C'est possible mais un peu compliqué: voir sur le site.

Une solution alternative (mais tout à fait criticable) est d'utiliser une composition, sans héritage:

In [231]:
class Doctorant:
    def __init__(self, nom, prénom, date_naissance, numéro_étudiant, laboratoire):
        self.étudiant = Étudiant(nom, prénom, date_naissance, numéro_étudiant)
        self.enseignant_chercheur = EnseignantChercheur(nom, prénom, date_naissance, laboratoire)

print(Doctorant.mro())
[<class '__main__.Doctorant'>, <class 'object'>]

Critiques:

  • On n'hérite pas des attributs et méthodes de Étudiant, EnseignantChercheur: il faut les redéfinir en faisant appel aux composants.
  • On dédouble certaines informations (nom, prénom, date_naissance).

Morale: évitez l'héritage multiple ou soyez pointilleux en l'utilisant.

À retenir sur l'héritage¶

  • Un mécanisme très important de programmation objet
  • Relation "être" pour construire des classes plus spécifiques
  • On factorise le code commun dans la classe mère
  • On ajoute des spécificités: attributs, méthodes
  • On surcharge les méthodes si besoin
  • super: pour faire appel aux méthodes de la classe mère depuis la classe fille
  • Héritage multiple possible mais délicat à mettre en oeuvre

Attributs vs notions de property, de getter/accesseur et de setter/mutateur¶

Attribut ou méthode?¶

Problématique: dans un objet p de type Polygone, on souhaiterait pouvoir obtenir le nombre de sommets. Deux logiques s'affrontent:

  • le nombre de sommets est une donnée, donc il faudrait y accéder par p.nb_sommets
  • le nombre de sommets s'obtient par l'action de compter les sommets, donc il faudrait y accéder par p.calcule_nb_sommets()

Attribut ou méthode: quelle logique vous semble meilleure?

In [ ]:
 

Remarque:

  • attribut: c'est la logique de l'utilisateur, la logique du qu'est-ce que c'est
  • méthode: c'est la logique de l'auteur du code, la logique du comment on fait

Réponse: on fait les deux en même temps grâce à une property!

  • pour l'utilisateur: accès à l'information par p.nb_sommets
  • mais ce n'est pas un attribut
  • c'est une méthode déguisée en attribut grâce au décorateur @property
  • l'auteur du code écrit une méthode
  • l'utilisateur n'y voit que du feu
classDiagram
    Polygone <|-- Carré
    Polygone <|-- Triangle
    Carré *-- Point
    Triangle *-- Point
    class Polygone{
        sommets : tuple de Point
        nb_sommets : int
        Polygone(liste_sommets)
        périmètre() float
        aire() float
    }
In [232]:
class Polygone:
    def __init__(self, sommets):
        self.sommets = tuple(sommets)

    @property
    def nb_sommets(self):
        return len(self.sommets)

p = Polygone([Point(0, 0), Point(1, 1), Point(2, 0)])
print("Nombre de sommets:", p.nb_sommets)
Nombre de sommets: 3

Autre exemple: remplacer Personne.calcule_age() par Personne.age

In [233]:
# Sans property
class Personne:
    def __init__(self, nom, prénom, date_naissance):
        self.nom = nom
        self.prénom = prénom
        self.date_naissance = date_naissance

    def calcule_age(self):
        return int((date.today() - self.date_naissance).days / 365)

p = Personne("Turing", "Alan", date(1936, 6, 23))
print(p.calcule_age())
88
In [234]:
# Avec property
class Personne:
    domaine = 'univ-amu.fr'
    
    def __init__(self, nom, prénom, date_naissance):
        self.nom = nom
        self.prénom = prénom
        self.date_naissance = date_naissance

    @property
    def age(self):
        return int((date.today() - self.date_naissance).days / 365)

p = Personne("Turing", "Alan", date(1936, 6, 23))
print(p.age)
88

À retenir sur la notion accesseurs/getter/property¶

  • accès comme pour un attribut objet.propriété: l'utilisateur n'a pas à savoir comment c'est implémenté derrière
  • implémentation comme une méthode avec @property
  • s'applique aux méthodes sans argument (ce qui est logique pour une donnée)

Accesseurs et mutateur (getter et setter)¶

Comment protéger un attribut d'utilisation erronée?

In [235]:
class Dé:
    def __init__(self, valeur=1):
        self.valeur = valeur
        
    def lancer(self):
        self.valeur = random.randrange(1, 7)

d = Dé()
d.valeur = -1  # Utilisation erronée!
print(d.valeur)
-1

Idée:

  • cacher l'attribut (en utilisant un nom commençant par _)
  • accès à l'attribut uniquement par des méthodes
  • une méthode de lecture (getter/accesseur) et une méthode d'écriture (setter/mutateur)

Une implémentation typique (dans d'autres langages):

In [236]:
class Dé:
    def __init__(self, valeur=1):
        self._valeur = valeur  # attribut _valeur au lieu de valeur
        
    def get_valeur(self):  # méthode de lecture de la valeur
        return self._valeur

    def set_valeur(self, valeur):  # méthode d de la valeur
        if 1 <= valeur <= 6:
            self._valeur = valeur

d = Dé()
d.set_valeur(-1)  # Utilisation erronée impossible!
print(d.get_valeur())
1

En python, une syntaxe plus pratique pour que l'utilisateur ne voie pas la différence avec un attribut:

In [237]:
class Dé:
    def __init__(self, valeur=1):
        self._valeur = 1
        self.valeur = valeur  # ici, self.valeur appelle le setter, il n'y a pas d'attribut valeur

    @property
    def valeur(self):
        return self._valeur

    @valeur.setter
    def valeur(self, valeur):
        if 1 <= valeur <= 6:
            self._valeur = valeur

d = Dé(-2)
d.valeur = -1  # Utilisation erronée impossible!
print(d.valeur)
1

À retenir:

  • on peut cacher un attribut pour le protéger
  • c'est transparent pour l'utilisateur, qui continuer à accéder à l'attribution via la notation instance.attribut
  • en lecture, instance.attribut appelle le getter défini par la méthode attribut décorée par @property, sans argument
  • en écriture, instance.attribut appelle le setter défini par la méthode attribut décorée par @attribut.setter et qui prend en argument la valeur à assigner
  • si vous voulez écrire un code très propre, faites ainsi pour tous les attributs
  • rien ne change pour l'utilisateur
  • cela fait plus de travail à l'écriture de la classe

Méthodes statiques¶

Une méthode statique est

  • une méthode qui dépend de la classe mais pas de ses instances
  • que l'on code avec le décorateur @staticmethod, sans self
  • utilité: par exemple pour construire certaines instances particulières
In [238]:
SEPT, HUIT, NEUF, DIX, VALET, DAME, ROI, AS = "sept", "huit", "neuf", "dix", "valet", "dame", "roi", "as"

PIQUE, COEUR, CARREAU, TREFLE = "pique", "cœur", "carreau", "trèfle"


VALEURS = (SEPT, HUIT, NEUF, DIX, VALET, DAME, ROI, AS)
COULEURS = (TREFLE, CARREAU, COEUR, PIQUE)


class Carte:
    def __init__(self, valeur, couleur):
        self.couleur = couleur
        self.valeur = valeur

    def __str__(self):
        return self.valeur + " de " + self.couleur

    @staticmethod
    def créer_jeu32():
        jeu = []
        for v in VALEURS:
            for c in COULEURS:
                jeu.append(Carte(valeur=v, couleur=c))
        return jeu 

j = Carte.créer_jeu32()
for c in j:
    print(c)
sept de trèfle
sept de carreau
sept de cœur
sept de pique
huit de trèfle
huit de carreau
huit de cœur
huit de pique
neuf de trèfle
neuf de carreau
neuf de cœur
neuf de pique
dix de trèfle
dix de carreau
dix de cœur
dix de pique
valet de trèfle
valet de carreau
valet de cœur
valet de pique
dame de trèfle
dame de carreau
dame de cœur
dame de pique
roi de trèfle
roi de carreau
roi de cœur
roi de pique
as de trèfle
as de carreau
as de cœur
as de pique