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 objetA
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 deA
, qui spécialisentA
. - vocabulaire: quand
B
,C
,D
héritent deA
, ce sont des classes filles ou sous-classes deA
,A
est leur classe mère. - symbole UML: flèche vide
classDiagram A <|-- B A <|-- C A <|-- D
- Un
LivretA
est unCompte
. UnCompteCourant
est unCompte
. - Ce sont des comptes particuliers. Un
Compte
n'est pas forcément unLivretA
.
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 unePersonne
. UnEnseignantChercheur
est unePersonne
. - 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 unPolygone
, unCarré
est unPolygone
. - Ce sont des polygones particuliers. Un
Polygone
n'est pas forcément unTriangle
.
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 unRectangle
qui estQuadrilatère
qui est unPolygone
, donc unCarré
est unPolygone
. - héritage multiple : un
Carré
est unRectangle
spécifique et unLosange
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 dansA
et automatiquement hérité dans les classes fillesB
,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¶
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)
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
prof = EnseignantChercheur("Turing", "Alan", date(1936, 6, 23), "Maths Cambridge")
print(prof.nom)
print(prof.calcule_age())
Turing 88
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
danssuper().__init__(...)
; voir le tuto sur super.
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.
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
print(EnseignantChercheur.mro())
[<class '__main__.EnseignantChercheur'>, <class '__main__.Personne'>, <class 'object'>]
Exemple complet 1: étudiants et enseignants-chercheurs¶
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()}>"
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})'
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 dansPersonne
- 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 }
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
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
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
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
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 dese_faire_taper
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)
Une classe fille Magicien
héritant de Personnage:
- un attribut supplémentaire
attaque_magique
- un
Magicien
peut lancer des sorts: méthode supplémentairelancer_sort
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) }
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:
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?
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 }
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
# 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
# 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?
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):
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:
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éthodeattribut
décorée par@property
, sans argument - en écriture,
instance.attribut
appelle le setter défini par la méthodeattribut
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
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