Aller au contenu

Chapitre 2. Programmation orientée objet (POO)

Évaluation des prérequis

Correction du QCM (Google Colab)

Activités d’introduction

Activité 1. Voitures

Objectif. Découvrir le vocabulaire de la POO

Enoncé

Pour découvrir le vocabulaire de la POO, prenons l'exemple de voitures.

Définir la catégorie d'objets Voiture c'est lister l'ensemble de ses caractéristiques et fonctionnalités. Par exemple donnons une définition minimale d'une catégorie Voiture.

  • La catégorie Voiture a pour caractéristiques :
    • couleur
    • nombre de places
    • motorisation
  • La catégorie Voiture a pour fonctionnalités :
    • avancer
    • reculer
    • freiner
    • tourner à droite ou à gauche
  • Une voiture_1 en particulier est une représentante de la catégorie Voiture. La valeur de chacune de ses caractéristiques est spécifique. Par exemple :
    • couleur : 'rouge'
    • nombre de places : 5
    • motorisation : 'essence'
    Les valeurs des caractéristiques peuvent être différentes ou identiques entre deux représentantes de la catégorie Voiture. Par exemple, une voiture_2 pourrait avoir les caractéristiques suivantes :
    • couleur : 'rouge'
    • nombre de places : 7
    • motorisation : 'électrique'
  • Contrairement aux valeurs des caractéristiques qui peuvent être différentes entre représentants de la catégorie Voiture, tous les représentants de la catégorie Voiture partagent les mêmes fonctionnalités c'est-à-dire :
    • avancer
    • reculer
    • freiner
    • tourner à droite ou à gauche

Compléter le tableau ci-après avec les mots suivants : Classe, Instance, Méthodes, Attributs.

Vocabulaire courant Vocabulaire de la POO
Catégorie d'objets ... d'objets
Représentant d'une catégorie d'objets ... d'une classe d'objets
Caractéristiques d'une catégorie d'objets ... d'une classe d'objets
Fonctionnalités d'une catégorie d'objets ... d'une classe d'objets
Correction
Vocabulaire courant Vocabulaire de la POO
Catégorie d'objets Classe d'objets
Représentant d'une catégorie d'objets Instance d'une classe d'objets
Caractéristiques d'une catégorie d'objets Attributs d'une classe d'objets
Fonctionnalités d'une catégorie d'objets Méthodes d'une classe d'objets

Activité 2. Danse de tortues

Objectif. Utiliser les méthodes d’une classe d’objets prédéfinie

Activité 3. Points du plan

Objectif. Définir une nouvelle classe d’objets

Travaux pratiques

TP1. Points, segments et polygones

Objectifs. Définir des classes d’objets, programmer avec des objets

TP2. Listes, files et piles

Objectif. Implémenter listes, files et piles par des classes d’objets sans utiliser les types construits Python préexistants (tuple, list, dict)

TP3. Bulles

Objectif. Programmmer une animation à partir de classes d’objets

Énoncé

Remarque. Exercice adapté du sujet de Bac Mayotte-Liban, 18 mai 2022

Le but de cette activité est de simuler le comportement d'une mousse composées de bulles multicolores qui se déplacent et fusionnent entre elles. La vidéo suivante montre le résutat final souhaité.

Voici l'ensemble des règles qui définissent le comportement de la mousse :

  • La fenêtre d'affichage est un carré de 400 pixels de côté.
  • La mousse est constituée au départ d'un nombre défini de bulles dont les couleurs, les positions les vitesses et les tailles respectives sont générées aléatoirement.
  • Les bulles ne peuvent pas s'échapper : elles rebondissent sur les bords de la fenêtre d'affichage.
  • Lorsque deux bulles se rencontrent, elles fusionnent. La bulle la plus petite est absorbée par la bulle la plus grande qui conserve sa couleur, augmente de taille et voit sa vitesse diminuer.
  • À chaque fusion, une nouvelle bulle remplace la bulle qui vient d'être absorbée.

Pour commencer le TP, ouvrir et forker ce projet Replit à compléter. Ce projet Replit est divisé en trois fichiers :

  • dessin.py : module fournissant des fonctions de dessin. Le code de ce module ne sera pas étudié.
  • bulles.py : module contenant les déclarations incomplètes des classes Bulles et Mousse. Ce fichier sera modifié au fur et à mesure de l'activité.
  • main.py : module principal de test. Le code de chaque question est réuni dans une fonction à tester. Pour certaines questions, il est demandé de modifier le code à tester.

Question 1.

  1. Dans le console Unix, saisir et valider la commande python main.py pour tester l'instanciation et l'affichage d'une bulle. Relancer plusieurs fois la commande pour constater la variabilité aléatoire. Quelle fonction Python standard a été utilisée pour obtenir ce comportement aléatoire ? Afficher avec la fonction print les coordonnées de la bulle affichée. Que peut-on en déduire sur la position de l'origine du repère et l'orientation des axes ?
  2. Dans le fichier main.py, modifier le code à tester de la fonction question1 afin d'instancier et d'afficher deux bulles simultanément. Vérifier ensuite le résultat.
  3. On rappelle que l'aire d'un disque est donnée par la formule \(\mathscr{A}_{\text{disque}} = \pi r^2\) où \(r\) désigne le rayon du disque. Ajouter à la classe Bulle une méthode aire qui renvoie l'aire de self en pixels carrés. On pensera à importer la constante pi depuis le module math. Compléter ensuite le code à tester de la fonction question1 pour tester la méthode aire par un affichage dans la console.
  4. On se place dans un repère orthonormé. Soit deux points \(A(x_A\,; y_A)\) et \(B(x_B\,; y_B)\). La distance séparant ces deux points est donnée par la formule \(AB = \sqrt{(x_B - x_A)^2 + (y_B - y_A)^2}\). Ajouter à la classe Bulle une méthode distance_centres qui renvoie la distance entre les centres de self et d'une autre bulle. On pensera à importer la fonction sqrt du module math. Tester ensuite votre code comme dans la question précédente.
  5. Afin de déterminer quand fusionner deux bulles, il faut établir si deux bulles sont entrées en contact l'une avec l'autre. Dans la classe Bulles, modifier la méthode bulles_en_contact afin qu'elle renvoie True si self et une autre bulle sont en contact et False dans le cas contraire. Aide. La figure ci-dessous donne un critère indiquant si deux bulles sont en contact ou non.
    1. Question 2.

      Dans cette question, est présentée la méthode d'animation. Le principe est de définir une fonction pas qui représente une étape élémentaire de l'animation. Cette étape élémentaire sera répétée avec une fréquence de 25 répétitions par seconde pour donner l'illusion du mouvement. Dans cette question, l'étape élémentaire consiste simplement à l'instanciation et l'affichage d'une bulle. On va donc accumuler de nombreuses bulles dans la fenêtre d'affichage. Tout en bas du fichier main.py, ajouter le caractère # avant question1() afin de neutraliser cette commande. Retirer juste en dessous le caractère # avant question2() puis exécuter le programme main.py. Expliquez pourquoi les bulles sont rassemblées dans un carré au centre de la fenêtre d'affichage.

      Question 3.

      Tout en bas du fichier main.py, ajouter le caractère # avant question2() afin de neutraliser cette commande. Retirer juste en dessous le caractère # avant question3() puis exécuter le programme main.py. Modifier le code de la fonction question3 afin d'afficher et déplacer simultanément deux bulles. Dans la méthode deplacer de la classe Bulles à quoi servent les deux branchements conditionnels ? Dans la méthode fusion de la classe Bulle, ajouter des commentaires afin d'expliquer le rôle de chaque ligne.

      Question 4.

      Tout en bas du fichier main.py, neutraliser la commande question3() et activer la commande question4(). Exécuter le code activé. Lorsqu'on crée un objet de la classe Mousse, une série d'objets de la classe Bulle sont créés et stocké dans une liste Python. Quel est le nombre de bulles créées par défaut à l'instanciation d'un objet de classe Mousse ? Modifier le code de la fonction question4 pour que 15 bulles soient affichées simultanément.

      Question 5.

      Dans la classe Mousse à quoi sert la méthode afficher ? En s'inspirant de cette méthode, compléter la méthode deplacer de la classe Mousse. Tout en bas du fichier main.py, neutraliser la commande question4() et activer la commande question5(). Exécuter le code activé pour vérifier le bon déplacement des bulles.

      Question 6.

      La méthode collision de la classe Mousse sert à prendre en charge l'ensemble des évenements lorsque deux bulles entrent en contact :
      • Identification de la grande bulle et de la petite bulle ;
      • fusion des bulles ;
      • remplacement dans la liste de la bulle absorbée par une nouvelle bulle et affichage de cette nouvelle bulle.
      Compléter la méthode collision. Tout en bas du fichier main.py, neutraliser la commande question5() et activer la commande question6(). Exécuter le code activé pour vérifier la fusion des bulles. Dans la classe Mousse, à quoi sert la méthode fusions ?

Exercices type Bac

Laser game (Métropole, 11 mai 2022)

Les participants à un jeu de laser game sont répartis en équipes et s’affrontent dans ce jeu de tir, revêtus d’une veste à capteurs et munis d’une arme factice émettant des infrarouges. Les ordinateurs embarqués dans ces vestes utilisent la programmation orientée objet pour modéliser les joueurs. La classe Joueur est définie comme suit :

class Joueur:

    def __init__(self, pseudo, identifiant, equipe):
        """Constructeur"""
        self.pseudo = pseudo
        self.equipe = equipe
        self.id = identifiant
        self.nb_de_tirs_emis = 0
        self.liste_id_tirs_recus = []
        self.est_actif = True

    def tire(self):
        """Méthode déclenchée par l'appui sur la gachette"""
        if self.est_actif:
            self.nb_de_tirs_emis = self.nb_de_tirs_emis + 1

    def est_determine(self):
        """Méthode qui renvoie True si le joueur réalise un grand nombre de tirs"""
        return self.nb_de_tirs_emis > 500

    def subit_un_tir(self, id_recu):
        """Méthode déclenchée par les capteurs de la veste"""
        if self.est_actif:
            self.est_actif = False
            self.liste_id_tirs_recus.append(id_recu)
  1. Parmi les instructions suivantes, recopier celle qui permet de déclarer un objet joueur1, instance de la classe Joueur, correspondant à un joueur dont le pseudo est "Sniper", dont l’identifiant est 319 et qui est intégré à l’équipe "A":
    • Instruction 1 : joueur1 = ["Sniper", 319, "A"]
    • Instruction 2 : joueur1 = new Joueur["Sniper", 319, "A"]
    • Instruction 3 : joueur1 = Joueur("Sniper", 319, "A")
    • Instruction 4 : joueur1 = Joueur{"pseudo": "Sniper", "id": 319, "equipe": "A"}
    Réponse attendue

    Instruction 3 : joueur1 = Joueur("Sniper", 319, "A")

    Explications. Pour déclarer une instance de la classe Joueur, on écrit le nom de la classe suivi de parenthèses contenant la liste des valeurs prises par les paramètres positionnels obligatoires du constructeur de cette classe, ici pseudo, identifiant, equipe.

  2. La méthode subit_un_tir réalise les actions suivantes. Lorsqu’un joueur actif subit un tir capté par sa veste, l’identifiant du tireur est ajouté à l’attribut liste_id_tirs_recus et l’attribut est_actif prend la valeur False (le joueur est désactivé). Il doit alors revenir à son camp de base pour être de nouveau actif.
    1. Écrire la méthode redevenir_actif qui rend à nouveau le joueur actif uniquement s’il était précédemment désactivé.
      Réponse attendue

      Le symbole ... représente ci-après le début de la classe Joueur qui n'a pas été écrit. On propose ci-dessous deux solutions possibles, la première étant plus élégante, utilisant l'opérateur logique not.

      Solution 1.

      class Joueur:
          ...
          def redevenir_actif(self):
              if not self.est_actif:
                  self.est_actif = True
      

      Solution 2.

      class Joueur:
          ...
          def redevenir_actif(self):
              if self.est_actif == False:
                  self.est_actif = True
      

      Remarque. La méthode redevenir_actif est une opération de type opérateur : elle modifie l'instance de classe en changeant la valeur de son attribut est_actif. La méthode redevenir_actif ne renvoie donc rien d'où l'absence du mot-clé return.

    2. Écrire la méthode nb_de_tirs_recus qui renvoie le nombre de tirs reçus par un joueur en utilisant son attribut liste_id_tirs_recus.
      Réponse attendue

      Le symbole ... ci-après représente le début de la classe Joueur qui n'a pas été écrit.

      class Joueur:
          ...
          def nb_de_tirs_recus(self):
              return len(self.liste_id_tirs_recus)
      

      Explications. Dans le constructeur __init__ de la classe Joueur, l'attribut liste_id_tirs_recus est initialisé comme liste vide []. La méthode subit_un_tir de la classe Joueur ajoute en bout de cette liste (avec la méthode de liste append) l'identifiant de chaque joueur qui touche self avec son laser. Ainsi, pour connaître le nombre de tirs reçus, il suffit de connaître la longueur de la liste liste_id_tirs_recus obtenu à l'aide de la fonction len. La méthode nb_de_tirs_recus est une opération du type accesseur : elle ne modifie pas l'instance de classe mais fournit une information sur elle : le nombre de tirs recus. On utilise donc le mot-clé return pour renvoyer cette information.

  3. Lorsque la partie est terminée, les participants rejoignent leur camp de base respectif où un ordinateur, qui utilise la classe Base, récupère les données. La classe Base est définie par :
    • ses attributs :
      • equipe : nom de l’équipe (str), par exemple "A" ;
      • liste_des_id_de_l_equipe qui correspond à la liste (list) des identifiants connus des joueurs de l’équipe ;
      • score : score (int) de l’équipe dont la valeur initiale est 1000 ;
    • ses méthodes :
      • est_un_id_allie qui renvoie True si l’identifiant passé en paramètre est un identifiant d’un joueur de l’équipe, False sinon ;
      • incremente_score qui fait varier l’attribut score du nombre passé en paramètre ;
      • collecte_information qui récupère les statistiques d’un participant passé en paramètre (instance de la classe Joueur) pour calculer le score de l’équipe .
    On présente ci-dessous la méthode collecte_information de la classe Base. Les caractères ... représentent le début de la classe Base qui n'a pas été écrit.
    class Base:
        ...
        def collecte_information(self, participant):
            if participant.equipe == self.equipe : # test 1
                for id in participant.liste_id_tirs_recus:
                    if self.est_un_id_allie(id): # test 2
                        self.incremente_score(-20)
                    else:
                        self.incremente_score(-10)
    
    1. Indiquer le numéro du test (test 1 ou test 2) qui permet de vérifier qu’en fin de partie un participant égaré n’a pas rejoint par erreur la base adverse.
      Réponse attendue

      test 1.

      Explications. Lorsqu'un participant (joueur) s'approche d'une base, la méthode collecte_information doit prendre en compte le joueur uniquement si son équipe correspond à celle de la base. Cela est assuré par le test participant.equipe == self.equipeparticipant représente le joueur et self représente la base. Si le joueur est bien de l'équipe de la base, alors la boucle for parcours la liste des identifiants de joueurs à l'origine de tirs reçus par le joueur et incrémente le score. Le test 2. permet pour chaque tir reçu de savoir si ce tir provient d'un joueur ennemi ou allié.

    2. Décrire comment varie quantitativement le score de la base lorsqu’un joueur de cette équipe a été touché par le tir d’un coéquipier.
      Réponse attendue

      Lorsqu'un joueur de cette équipe a été touché par le tir d'un coéquipier (si self.est_un_id_allie(id) renvoie True, test 2.), 20 points sont retirés au score de l'équipe.

  4. On souhaite accorder à une base un bonus de 40 points pour chaque joueur de son équipe particulièrement déterminé (qui réalise un grand nombre de tirs). On a réécrit ci-dessous la méthode collecte_information que l'on souhaite modifier.
    class Base:
        ...
        def collecte_information(self, participant):
            if participant.equipe == self.equipe : # test 1
                for id in participant.liste_id_tirs_recus:
                    if self.est_un_id_allie(id): # test 2
                        self.incremente_score(-20)
                    else:
                        self.incremente_score(-10)
                # si le participant réalise un grand nombre de tirs
                ...
                    # le score de la base augmente de 40 points
                    ...
    
    Recopier et compléter la méthode collecte_information en remplaçant les deux derniers ... par les bonnes insructions. Utiliser les méthodes des classes Joueur et Base.
    Réponse attendue
    class Base:
        ...
        def collecte_information(self, participant):
            if participant.equipe == self.equipe : # test 1
                for id in participant.liste_id_tirs_recus:
                    if self.est_un_id_allie(id): # test 2
                        self.incremente_score(-20)
                    else:
                        self.incremente_score(-10)
                # si le participant réalise un grand nombre de tirs
                if participant.est_determine():
                    # le score de la base augmente de 40 points
                    self.incremente_score(40)
    

    Remarque. On utilise la méthode est_determine de la classe Joueur pour tester la "détermination" du joueur participant. C'est une méthode de classe, attention à penser aux parenthèses ! On ne précise aucun argument dans les parenthèses car cette méthode prend pour unique paramètre self. L'examen de cette méthode dans la classe Joueur indique qu'un joueur est considéré comme déterminé s'il a réalisé plus de 500 tirs.

  5. On suppose l'existence d'une troisième classe Equipe dont on donne une extrait de la définition ci-dessous.
    class Equipe:
    
        def __init__(self, nom):
            self.nom = nom
            self.liste_joueurs = []
    
        def ajouter_joueur(self, joueur):
            ...
    
    1. L'attribut self.liste_joueur de la classe Equipe est un tableau (type list) au départ vide qui va contenir la liste des joueurs de l'équipe, c'est-à-dire une liste d'objets de la classe Joueur. Compléter la définition de la méthode ajouter_joueur de la classe Equipe permettant d'ajouter un joueur à l'équipe.
      Réponse attendue
      class Equipe:
          ...
          def ajouter_joueur(self, joueur):
              self.liste_joueurs.append(joueur)
      
    2. Écrire une méthode est_determinee de la classe Equipe qui renvoie True si la proportion de joueurs déterminés de cette équipe est supérieure ou égale à 50% et False sinon.
      Réponse attendue
      class Equipe:
          ...
          def est_determinee(self):
              nb_joueurs_determines = 0
              for joueur in self.liste_joueurs:
                  if joueur.est_determine():
                      nb_joueurs_determines += 1
              return nb_joueurs_determines/len(self.liste_joueurs) >= 0.5