<string>

Table des matières

Mission 9 - Héritage

Mission 9 : Héritage

Mission 9 : Héritage

1   Introduction

Dans cette mission vous allez étendre votre logiciel de lecture de musique (Mission 8), en utilisant le concept de l'héritage, pour pouvoir traiter d'autres types de médias que les chansons, comme les vidéos, livres audio, etc.

2   Objectifs

A l'issue de cette mission, chacun d'entre vous :

  • pourra masquer les variables d'instance d'une classe et écrire des méthodes pour y accéder;

  • aura appris la notion de variable de classe et son utilité;

  • pourra expliquer en ses propre mots et illustrer par l'exemple les principes de l'héritage en Python:

    * l'héritage d'une classe;
    * la redéfinition de méthodes;
    * l'utilisation de ``self`` et ``super()``;
    
  • sera capable d'utiliser l'héritage pour étendre un programme Python;

  • pourra utiliser les méthodes magiques comme

    * ``__init__`` pour initialiser les objets d'une classe;
    * ``__str__`` pour retourner une représentation textuelle d'un instance d'une classe;
    * ``__eq__`` pour définir l'égalité entres objets d'une classe.
    

3   Préparation, étude et apprentissage

La matière relative à cette mission est décrite dans les sections suivantes de la partie Objects du syllabus en ligne:

ainsi que les annexes:

4   Questionnaire de démarrage

4.1   Questions à choix multiple

Les questions à choix multiples de cette mission sont accessibles en ligne depuis https://inginious.info.ucl.ac.be/course/LSINF1101-PYTHON/mission_9_QCM


        
        

4.2   Questions ouvertes

4.2.1   Egalité

Considérez une classe Pair :

class Pair:

    def __init__(self, x, y):
        self.a = x
        self.b = y

    def __str__(self):
        return str(self.a) + ", " + str(self.b)

Maintenant considérez le code suivant :

p1 = Pair(9, 42)
p2 = Pair(9, 42);
print(p1 == p2)

La dernière instruction affichera False. (Pourquoi?)

Ajoutez une méthode __eq__(self, p) à la classe Pair qui compare la valeur de deux paires, de manière à ce que print(p1 == p2) imprimera True au lieu de False.

Dans l'implémentation de votre méthode pensez également à gérer le cas où p == None.


        
        

4.2.2   L'héritage

L'héritage est un principe de base de la programmation orientée objet. Considérons les classes A, B, C et D ci-dessous :

class A :

  def m1(self) :
      print("A 1")

  def m2(self) :
      print("A 2")

  def m3(self) :
      self.m1()   # appel à la méthode m1 sur la même instance

  def nom(self) :
      return "A"

class B(A) :

  def m2(self):
      print("B 2")

class C(A):

    def m1(self) :
        print("C 1")

    def nom(self):
        return "C"

class D(C) :

    def m2(self) :
        print("D 2")

Considérant ces quatre classes, on vous demande de :

  • Expliquez ce que représente le mot self dans la définition de ces différentes méthodes.
 
 
 
 

  • Dessiner un diagramme reprenant ces quatre classes.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

  • Expliquer ce qui sera affiché lors de l'exécution des instructions Python ci-dessous :
a = A()
print(a.nom())
a.m1()
a.m2()
a.m3()
b = B()
print(b.nom())
b.m1()
b.m2()
b.m3()
c = C()
print(c.nom())
c.m1()
c.m2()
c.m3()
d = D()
print(d.nom())
d.m1()
d.m2()
d.m3()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

4.2.3   C'est super !

  • Expliquez le rôle de la fonction spécial super() dans le langage Python. Donnez un exemple.
 
 
 
 
 

  • Considérons les classes E et F ci-dessous :
class E :

    def m(self) :
        print("E 1")

    def n(self) :
        print("E 2")

    def p(self) :
        self.n()   # appel à la méthode n sur la même instance

class F(E) :

   def q(self) :
       print("F 1")

   def n(self) :
       super().n() # appeler la méthode définie sur la classe mère
       print("F 2")

   def r(self) :
        self.m() # appel à la méthode m sur la même instance

Expliquer ce qui sera affiché lors de l'exécution des instructions Python suivantes :

f = F()
f.q()
f.m()
f.r()
f.n()
f.p()
 
 
 
 
 
 
 

4.2.4   Rectangle

Considérons la classe Figure reprise ci-dessous :

class Figure:

    def __init__(self,x,y,visible=False) :
        """
        @pre x, y sont des entiers représentant des positions sur l'écran
        @post Une figure a été créée avec centre de gravité aux coordonnées x,y.
              Cette figure n'est initialement pas visible.
        """
        self.x = x
        self.y = y
        self.__visible = visible

    def est_visible(self) :
        """
        @pre -
        @post a retourné la visibilité de cette figure
        """
        return self.__visible

    def surface(self) :
        """
        @pre -
        @post la surface (un float) de la figure a été calculé et retournée
        """
        pass            # code non fourni

Cette classe Figure est la classe mère d'un ensemble de classes permettant de représenter des figures géométriques. Chaque figure géométrique est placée à une position (x,y) (centre de gravité) sur l'écran et la classe contient des variables d'instance et des méthodes permettant de manipuler cette figure géométrique (notamment des méthodes permettant d'afficher la figure à l'écran, mais ces méthodes ne sont pas reprises dans les extraits présentés dans cet exercice). Parmi ces figures géométriques, on trouve notamment la classe Rectangle qui hérite de la classe Figure et dont un fragment est repris ci-dessous :

class Rectangle(Figure):

    def __init__(self,longueur,largeur,x,y) :
        """
        @pre longueur et largeur sont des entiers positifs
             x, y sont des entiers représentant des positions sur l'écran
        @post un rectangle dont le centre de gravite est en x,y
              et ayant comme longueur lo et comme largeur la a été créé
        """
        super().__init__(x,y)
        self.longueur = longueur
        self.largeur = largeur

    def __str__(self) :
        return str((self.longueur,self.largeur,self.x,self.y,self.est_visible()))

>>> r = Rectangle(10,20,0,0)
>>> print(r)
(10, 20, 0, 0, False)

Maintenant expliquez :

  • Quelles sont les variables d'instance qu'une instance de la classe Rectangle peut utiliser ?
  • Que se passe-t il lorsqu'une instance r de la classe Rectangle est créé?
  • Que fait l'appel à super() dans la méthode __init__ de la classe Rectangle ?
  • Que se passe-t il si on met cet appel à super() comme dernière instruction dans la méthode __init__ de la classe Rectangle ?
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Dans la classe Rectangle, faut-il redéfinir les méthodes suivantes (si oui, écrivez le code de la nouvelle méthode - si non, expliquez pourquoi ):

  • surface()
 
 
 
 
 
 
 
 

  • est_visible()
 
 
 
 

4.2.5   Variables privés


        
        

4.2.6   Carré

Comment feriez-vous maintenant pour définir une classe Carre qui étend la classe Rectangle et permet de représenter un carré ?

  • Ecrivez la méthode d'initialisation de la classe Carre (un carré se construit en indiquant les coordonnées de son centre de gravité et la longueur de son côté)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

  • Quelles sont les méthodes que vous devez redéfinir dans la classe "Carre" ?
 
 
 
 

  • Soient deux classes Circle et Ellipse. Quelles relations peut-on envisager entre ces deux classes ?
 
 
 
 

  • Ecrivez une méthode perimetre() de la classe Rectangle qui retourne le périmètre du rectangle.
 
 
 
 

  • Que faudrait-il faire pour avoir une méthode perimetre de la classe Carre?
 
 
 
 

4.2.7   Similitude

Définissez une méthode __eq__ pour la classe Figure, telle que deux figures sont égales si leur surface est égale.

 
 
 
 

Que se passe-t il si on veut comparer deux rectangles ayant la même surface? Par exemple:

>>> r1 = Rectangle(10,40,0,0)
>>> r2 = Rectangle(2,200,0,0)
>>> r1 == r2
 
 
 
 

Que se passe-t il si on veut comparer un rectangle avec un carré ayant la même surface? Par exemple:

>>> r = Rectangle(10,40,0,0)
>>> c = Carre(20,0,0)
>>> print(r == c)
 
 
 
 

4.2.8   Variable de classe

Et voici finalement une dernière question pour illustrer l'utilisation d'une variable de classe.

Complétez la classe Ticket ci-dessous:

"""
Un ticket de parking
"""
class Ticket :

    __prochain_numero = 1  # variable de classe pour générer le numéro du ticket

    def __init__(self) :
        """
        @pre  -
        @post Crée un ticket avec un nouveau numéro.
              Les numéros sont attribués séquentiellement à partir de 1.
        """
            # A COMPLETER

    def numero(self):
        """
        @pre  -
        @post retourne le numero de billet
        """
        return self.__numero


        
        

Mission 9 : Héritage

Mission 9 : Héritage

1   Description

Suite au succès de votre logiciel de lecture de musique (Mission 8), vous avez décidé d'étendre sa fonctionnalité pour pouvoir lire d'autres types de médias que les chansons (vidéos, livres audio, ...).

Vous disposez pour commencer d'un fichier mission9.py comportant 3 classes:

  1. Duree: représente une durée dans le temps dans le format hh:mm:ss. Usage similaire à la classe du même nom de la Mission 8.
  2. Media: représente un média générique, comme une chanson, un livre audio ou une vidéo qui peut être "joué" pour être écouté, lu ou regardé.
  3. ListeLecture: représente une compilation nommée de médias.

Un fichier de test initial test.py est également fourni. Exécuter ce fichier test affichera l'exemple de liste de lecture suivant:

[#1] Minecraft (4 medias)
01: (00:10:01, Média) 'Tuto installation Minecraft (100% gratuit!!)' par LeCrafteur
02: (00:03:36, Média) 'Sweden' par C418
03: (00:04:24, Média) 'Revenge' par CaptainSparklez
04: (02:36:21, Média) 'Journal d'un noob (tome 1)' par Cube Kid

Vous remarquerez le [#1] au début de l'affichage. Ceci représente l'identifiant unique d'une liste de lecture.

L'objectif est d'étendre la fonctionnalité des Media en créant de nouvelles classes, plus spécialisées, qui en héritent tout en restant utilisables par la classe ListeLecture. L'avantage de cette approche est qu'elle nous permet d'implémenter de nouvelles fonctionnalités pour des cas particuliers de Media, sans pour autant devoir modifier le code de la classe ListeLecture pour supporter celles-ci. C'est un des avantages principaux de la programmation orientée objet.

Votre programme final devrait être capable de générer la playliste suivante:

[#3] Minecraft (4 medias)
01: (00:10:01, Vidéo) 'Tuto installation Minecraft (100% gratuit!!)' par LeCrafteur (720p)
02: (00:03:36, Chanson) 'Sweden' par C418 [Album: Minecraft OST]
03: (00:04:24, Chanson) 'Revenge' par CaptainSparklez (feat. Villageois, Herobrine) [Album: Fallen Kingdoms]
04: (02:36:21, Livre Audio) 'Journal d'un noob (tome 1)' par Cube Kid, édité par 404 Éditions

et l'analyse de tailles de fichiers suivant:

TOTAL : 238.01MB
[120.20MB] Tuto installation Minecraft (100% gratuit!!)
[10.80MB] Sweden
[13.20MB] Revenge
[93.81MB] Journal d'un noob (tome 1)
TOTAL : 238.01MB

2   Étapes

Voici les étapes à suivre.

2.1   Fichiers

Chargez les fichiers mission9.py et test.py et étudiez leurs contenus. Vous disposez des classes et fonctions suivantes:

  • Duree: cette classe représente une durée dans le temps. Elle contient les composantes en heures, minutes et secondes de la durée, une méthode qui calcule cette durée en secondes, ainsi qu'une méthode pour ajouter une durée à une autre.
  • Media: cette classe représente un média générique, ayant un titre, un auteur et une durée. Elle contient également la méthode taille() qui renvoie la taille en méga-octets du média, ainsi qu'une méthode taille_par_seconde(), utilisée par la précédente, qui renvoie la taille en méga-octets par seconde de durée. Cette méthode lève une erreur NotImplementedError par défaut, car elle dépend du type de média et donc devra être réécrite par les classes-filles. Pour finir, elle contient la méthode type_media() qui renvoie un string décrivant le type de média de l'objet. Par défaut ce type sera "Média".
  • ListeLecture: cette classe représente une compilation de Media. Elle a un identifiant unique, un nom, une méthode ajouter(media) pour ajouter un média à la liste de lecture, et une méthode __str__ qui imprime les informations de la playliste de manière formattée.
  • afficher_playliste: une fonction de test de la classe ListeLecture qui imprime la liste de lecture de l'exemple repris ci-dessus.

Vous devrez vous-mêmes créer au moins les deux classes Video et Chanson qui héritent de la classe Media, ainsi que des tests supplémentaires pour tester le bon fonctionnement des nouvelles fonctionnalités. Une classe LivreAudio est fournie pour servir d'exemple.

2.2   La classe LivreAudio

Cette classe représente un livre audio, et est une classe-fille de Media. Dans la méthode __init__, elle prend un argument en plus que celle de Media: editeur représente l'éditeur du livre associé.

La méthode taille_par_seconde() est remplacée par une nouvelle version qui renvoie 0.01, qui est la taille de lecture d'un livre audio par défaut en MB/sec. Observez comment la méthode taille() dans Media fera appel à cette version de la méthode taille_par_seconde(), et non à celle définie au départ dans Media [1]. De même, la méthode type_media() renvoie ici "Livre Audio" au lieu de "Média", et ce changement prend effet quand la méthode __str__() d'un média est appelé.

class LivreAudio(Media):
    """
    Représente un livre audio. En plus des attributs présents dans 'Media',
    'LivreAudio' inclu aussi un attribut représentant l'éditeur du livre.
    """

    def __init__(self, titre, auteur, duree, editeur):
        """
        @pre:  titre est un string
               auteur est un string
               duree est une instance de 'Duree'
               editeur est un string
        @post: un livre audio ayant les propriétés demandées
        """
        super().__init__(titre, auteur, duree)
        self.editeur = editeur

    def taille_par_seconde(self):
        """
        Renvoie la taille de lecture par défaut d'un livre audio
        en méga-octets par seconde
        """
        return 0.01

    def type_media(self):
        """
        Renvoie un string indiquant le type de média
        """
        return "Livre Audio"

    def __str__(self):
        s = super().__str__() + ", édité par " + self.editeur
        return s

Pour une playliste qui ne contient qu'un seul média de type LivreAudio, votre programme pourrait afficher un output comme:

[#2] Livres audio Minecraft (1 medias)
01: (02:36:21, Livre Audio) 'Journal d'un noob (tome 1)' par Cube Kid, édité par 404 Éditions

2.3   La classe Video

  • Au fichier mission9.py, ajoutez une nouvelle classe Video qui hérite de la classe Media. Cette nouvelle classe représente une vidéo que l'on pourrait visionner sur notre logiciel de lecture multimédia.

  • Définissez une méthode d'initialisation avec le titre, l'auteur, la durée et la résolution en paramètre. La résolution est un string qui ne peut prendre que quelques valeurs spécifiques, à savoir '720p', '1080p' ou '4K' (dans l'ordre de fidélité croissant). Une valeur non valable pour la résolution doit lever une ValueError.

  • Re-définissez la méthode taille_par_seconde() afin que chaque seconde de vidéo pèse 0.1 Mo, multiplié par 2, 5 ou 10 selon la résolution choisie.

  • Re-définissez la méthode __str__ afin d'ajouter des informations sur la résolution de la vidéo, en plus des informations déjà fournies dans la même méthode de classe-mère.

  • Dans le fichier test.py, ajoutez des tests pour tester cette nouvelle classe et son utilisation dans une playliste. Par exemple, vérifiez que la ligne correspondant à une vidéo soit affichée comme:

    01: (00:10:01, Vidéo) 'Tuto installation Minecraft (100% gratuit!!)' par LeCrafteur (720p)
    

2.4   La classe Chanson

  • Créez également une nouvelle classe Chanson qui hérite de la classe Media. Cette nouvelle classe représente une chanson, qui peut appartenir à un album et peut avoir d'autres artistes que l'auteur en featuring.

  • Définissez une méthode d'initialisation avec le titre, l'auteur, la durée, l'album (string) et les autres artistes featuring avec l'auteur (liste de strings). L'album a une valeur par défaut de None, et les featuring une liste vide.

  • Re-définissez la méthode taille_par_seconde() afin que chaque seconde pèse 0.05 Mo.

  • Re-définissez la méthode __str__ afin d'ajouter des informations sur l'album et les featurings de la chanson s'il y en a.

  • Dans le fichier test.py, ajoutez des tests pour tester cette nouvelle classe et son utilisation dans une liste de lecture. Par exemple, vérifiez que la ligne correspondant à une vidéo soit affichée comme:

    02: (00:03:36, Chanson) 'Sweden' par C418 [Album: Minecraft OST]
    03: (00:04:24, Chanson) 'Revenge' par CaptainSparklez (feat. Villageois, Herobrine) [Album: Fallen Kingdoms]
    

2.5   Identifiant des listes de lecture

Puisque l'identifiant d'une liste de lecture doit être unique, assurez bien lors de la création d'une instance de ListeLecture que l'identifiant est incrémenté à chaque fois par rapport à la dernière instance créée, ce qui veut dire que la toute première instance de ListeLecture aura comme valeur pour l'attribut id 1, la deuxième 2, la troisième 3 et ainsi de suite, de sorte que chaque instance ait un identifiant unique.

3   Remise de votre solution

Pour cette mission, vous devez soumettre toutes les classes de votre programme dans un seul fichier mission9.py, vos classes tests dans un fichier test.py, ainsi que votre fichier README.txt qui décrit comment on peut tester votre code.

Votre fichier mission9.py doit contenir les classes Duree, ListeLecture, Media, Video et Chanson.

Votre fichier test.py doit contenir des tests pour chacune des classes et étappes de cette mission, ainsi qu'une série d'instructions à la fin pour lancer tous les tests automatiquement lors de l'exécution du fichier.

L'exécution de votre programme mission9.py doit imprimer une liste de lecture et une analyse de tailles de fichier comme illustré plus haut dans ce document.


        
        

Footnotes

[1]Ce mécanisme où une méthode définie dans une classe mère peut appeler une méthode implémentée par une classe fille, sera vu plus en détail plus tard.
Questions complémentaires

Questions complémentaires

1   Variables d'instance

Quelqu'un a programmé une classe Compte représentant un compte bancaire avec 2 attributs privés __titulaire (représentant le titulaire du compte) et __solde (représentant le montant sur le compte, initialement zéro) et 1 attribut publique représentant la banque du compte.

class Compte :

    def __init__(self, banque, titulaire, solde = 0) :
        self.banque = banque
        self.__titulaire = titulaire
        self.__solde = solde

    def banque(self) :
        return self.banque

    def titulaire(self):
        return self.__titulaire

    def solde(self):
        return self.__solde

    def __str__(self) :
        return "Banque: " + self.banque() \
           + " Compte: " + self.titulaire() \
           + " Solde: " + str(self.solde())

a = Compte("ShittyBank","Kim")
print(a)

Malheureusement, quand on exécute l'instruction print(a), une erreur se produit:

Traceback (most recent call last):
  > print(a)
  > print(self.banque())
  > TypeError: 'str' object is not callable

Quel est le problème? Pouvez-vous corriger le code?

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

2   Variable de classe


        
        

3   Questions supplémentaires