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.
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.
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:
Les questions à choix multiples de cette mission sont accessibles en ligne depuis https://inginious.info.ucl.ac.be/course/LSINF1101-PYTHON/mission_9_QCM
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.
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 :
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()
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()
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 :
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 ):
Comment feriez-vous maintenant pour définir une classe Carre qui étend la classe Rectangle et permet de représenter un carré ?
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)
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
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:
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
Voici les étapes à suivre.
Chargez les fichiers mission9.py et test.py et étudiez leurs contenus. Vous disposez des classes et fonctions suivantes:
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.
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
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)
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]
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.
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. |
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?